Merge "Fix class name in test mapping." into main
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index 3a66081..c872516 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -3535,7 +3535,7 @@
      * {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_DEFAULT } mode.
      * They can be queried through
      * {@link android.hardware.camera2.CameraCharacteristics#get } with
-     * {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION) }.
+     * {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION }.
      * Unless reported by both
      * {@link android.hardware.camera2.params.StreamConfigurationMap }s, the outputs from
      * <code>{@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION android.scaler.streamConfigurationMapMaximumResolution}</code> and
@@ -3550,13 +3550,12 @@
      * <ul>
      * <li>
      * <p>The mandatory stream combinations listed in
-     *   {@link android.hardware.camera2.CameraCharacteristics.mandatoryMaximumResolutionStreamCombinations }
-     *   would not apply.</p>
+     *   {@link CameraCharacteristics#SCALER_MANDATORY_MAXIMUM_RESOLUTION_STREAM_COMBINATIONS android.scaler.mandatoryMaximumResolutionStreamCombinations}  would not apply.</p>
      * </li>
      * <li>
      * <p>The bayer pattern of {@code RAW} streams when
      *   {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }
-     *   is selected will be the one listed in {@link android.sensor.info.binningFactor }.</p>
+     *   is selected will be the one listed in {@link CameraCharacteristics#SENSOR_INFO_BINNING_FACTOR android.sensor.info.binningFactor}.</p>
      * </li>
      * <li>
      * <p>The following keys will always be present:</p>
@@ -3576,9 +3575,11 @@
      *
      * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
      *
+     * @see CameraCharacteristics#SCALER_MANDATORY_MAXIMUM_RESOLUTION_STREAM_COMBINATIONS
      * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP
      * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION
      * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION
+     * @see CameraCharacteristics#SENSOR_INFO_BINNING_FACTOR
      * @see CameraCharacteristics#SENSOR_INFO_PIXEL_ARRAY_SIZE_MAXIMUM_RESOLUTION
      * @see CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION
      * @see #SENSOR_PIXEL_MODE_DEFAULT
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 1536376..57f7bca 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -4460,7 +4460,7 @@
      * {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_DEFAULT } mode.
      * They can be queried through
      * {@link android.hardware.camera2.CameraCharacteristics#get } with
-     * {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION) }.
+     * {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION }.
      * Unless reported by both
      * {@link android.hardware.camera2.params.StreamConfigurationMap }s, the outputs from
      * <code>{@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION android.scaler.streamConfigurationMapMaximumResolution}</code> and
@@ -4475,13 +4475,12 @@
      * <ul>
      * <li>
      * <p>The mandatory stream combinations listed in
-     *   {@link android.hardware.camera2.CameraCharacteristics.mandatoryMaximumResolutionStreamCombinations }
-     *   would not apply.</p>
+     *   {@link CameraCharacteristics#SCALER_MANDATORY_MAXIMUM_RESOLUTION_STREAM_COMBINATIONS android.scaler.mandatoryMaximumResolutionStreamCombinations}  would not apply.</p>
      * </li>
      * <li>
      * <p>The bayer pattern of {@code RAW} streams when
      *   {@link android.hardware.camera2.CameraMetadata#SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION }
-     *   is selected will be the one listed in {@link android.sensor.info.binningFactor }.</p>
+     *   is selected will be the one listed in {@link CameraCharacteristics#SENSOR_INFO_BINNING_FACTOR android.sensor.info.binningFactor}.</p>
      * </li>
      * <li>
      * <p>The following keys will always be present:</p>
@@ -4501,9 +4500,11 @@
      *
      * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
      *
+     * @see CameraCharacteristics#SCALER_MANDATORY_MAXIMUM_RESOLUTION_STREAM_COMBINATIONS
      * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP
      * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION
      * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION
+     * @see CameraCharacteristics#SENSOR_INFO_BINNING_FACTOR
      * @see CameraCharacteristics#SENSOR_INFO_PIXEL_ARRAY_SIZE_MAXIMUM_RESOLUTION
      * @see CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION
      * @see #SENSOR_PIXEL_MODE_DEFAULT
diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java
index 299e7f1..e6bdfe1 100644
--- a/core/java/android/os/GraphicsEnvironment.java
+++ b/core/java/android/os/GraphicsEnvironment.java
@@ -115,7 +115,6 @@
     private static final String ANGLE_GL_DRIVER_CHOICE_DEFAULT = "default";
     private static final String ANGLE_GL_DRIVER_CHOICE_ANGLE = "angle";
     private static final String ANGLE_GL_DRIVER_CHOICE_NATIVE = "native";
-    private static final String SYSTEM_ANGLE_STRING = "system";
 
     private static final String PROPERTY_RO_ANGLE_SUPPORTED = "ro.gfx.angle.supported";
 
@@ -196,16 +195,15 @@
     }
 
     /**
-     * Query to determine the ANGLE driver choice.
+     * Query to determine if ANGLE should be used
      */
-    private String queryAngleChoice(Context context, Bundle coreSettings,
-                                               String packageName) {
+    private boolean shouldUseAngle(Context context, Bundle coreSettings, String packageName) {
         if (TextUtils.isEmpty(packageName)) {
             Log.v(TAG, "No package name specified; use the system driver");
-            return ANGLE_GL_DRIVER_CHOICE_DEFAULT;
+            return false;
         }
 
-        return queryAngleChoiceInternal(context, coreSettings, packageName);
+        return shouldUseAngleInternal(context, coreSettings, packageName);
     }
 
     private int getVulkanVersion(PackageManager pm) {
@@ -426,11 +424,10 @@
      *    forces a choice;
      * 3) Use ANGLE if isAngleEnabledByGameMode() returns true;
      */
-    private String queryAngleChoiceInternal(Context context, Bundle bundle,
-                                                       String packageName) {
+    private boolean shouldUseAngleInternal(Context context, Bundle bundle, String packageName) {
         // Make sure we have a good package name
         if (TextUtils.isEmpty(packageName)) {
-            return ANGLE_GL_DRIVER_CHOICE_DEFAULT;
+            return false;
         }
 
         // Check the semi-global switch (i.e. once system has booted enough) for whether ANGLE
@@ -445,7 +442,7 @@
         }
         if (allUseAngle == ANGLE_GL_DRIVER_ALL_ANGLE_ON) {
             Log.v(TAG, "Turn on ANGLE for all applications.");
-            return ANGLE_GL_DRIVER_CHOICE_ANGLE;
+            return true;
         }
 
         // Get the per-application settings lists
@@ -468,8 +465,7 @@
                             + optInPackages.size() + ", "
                         + "number of values: "
                             + optInValues.size());
-            return mEnabledByGameMode ? ANGLE_GL_DRIVER_CHOICE_ANGLE
-                    : ANGLE_GL_DRIVER_CHOICE_DEFAULT;
+            return mEnabledByGameMode;
         }
 
         // See if this application is listed in the per-application settings list
@@ -477,8 +473,7 @@
 
         if (pkgIndex < 0) {
             Log.v(TAG, packageName + " is not listed in per-application setting");
-            return mEnabledByGameMode ? ANGLE_GL_DRIVER_CHOICE_ANGLE
-                    : ANGLE_GL_DRIVER_CHOICE_DEFAULT;
+            return mEnabledByGameMode;
         }
         mAngleOptInIndex = pkgIndex;
 
@@ -489,14 +484,13 @@
                 "ANGLE Developer option for '" + packageName + "' "
                         + "set to: '" + optInValue + "'");
         if (optInValue.equals(ANGLE_GL_DRIVER_CHOICE_ANGLE)) {
-            return ANGLE_GL_DRIVER_CHOICE_ANGLE;
+            return true;
         } else if (optInValue.equals(ANGLE_GL_DRIVER_CHOICE_NATIVE)) {
-            return ANGLE_GL_DRIVER_CHOICE_NATIVE;
+            return false;
         } else {
             // The user either chose default or an invalid value; go with the default driver or what
             // the game mode indicates
-            return mEnabledByGameMode ? ANGLE_GL_DRIVER_CHOICE_ANGLE
-                    : ANGLE_GL_DRIVER_CHOICE_DEFAULT;
+            return mEnabledByGameMode;
         }
     }
 
@@ -563,12 +557,8 @@
      */
     private boolean setupAngle(Context context, Bundle bundle, PackageManager packageManager,
             String packageName) {
-        final String angleChoice = queryAngleChoice(context, bundle, packageName);
-        if (angleChoice.equals(ANGLE_GL_DRIVER_CHOICE_DEFAULT)) {
-            return false;
-        }
-        if (angleChoice.equals(ANGLE_GL_DRIVER_CHOICE_NATIVE)) {
-            nativeSetAngleInfo("", true, packageName, null);
+
+        if (!shouldUseAngle(context, bundle, packageName)) {
             return false;
         }
 
@@ -637,10 +627,10 @@
             Log.d(TAG, "ANGLE package libs: " + paths);
         }
 
-        // If we make it to here, ANGLE apk will be used.  Call nativeSetAngleInfo() with the
-        // application package name and ANGLE features to use.
+        // If we make it to here, ANGLE will be used.  Call setAngleInfo() with the package name,
+        // and features to use.
         final String[] features = getAngleEglFeatures(context, bundle);
-        nativeSetAngleInfo(paths, false, packageName, features);
+        setAngleInfo(paths, false, packageName, features);
 
         return true;
     }
@@ -662,10 +652,10 @@
             return false;
         }
 
-        // If we make it to here, system ANGLE will be used.  Call nativeSetAngleInfo() with
-        // the application package name and ANGLE features to use.
+        // If we make it to here, ANGLE will be used.  Call setAngleInfo() with the package name,
+        // and features to use.
         final String[] features = getAngleEglFeatures(context, bundle);
-        nativeSetAngleInfo(SYSTEM_ANGLE_STRING, false, packageName, features);
+        setAngleInfo("", true, packageName, features);
         return true;
     }
 
@@ -946,8 +936,8 @@
     private static native void setDriverPathAndSphalLibraries(String path, String sphalLibraries);
     private static native void setGpuStats(String driverPackageName, String driverVersionName,
             long driverVersionCode, long driverBuildTime, String appPackageName, int vulkanVersion);
-    private static native void nativeSetAngleInfo(String path, boolean useNativeDriver,
-            String packageName, String[] features);
+    private static native void setAngleInfo(String path, boolean useSystemAngle, String packageName,
+            String[] features);
     private static native boolean setInjectLayersPrSetDumpable();
     private static native void nativeToggleAngleAsSystemDriver(boolean enabled);
 
diff --git a/core/jni/android_os_GraphicsEnvironment.cpp b/core/jni/android_os_GraphicsEnvironment.cpp
index 8fc30d1..afc3cbd 100644
--- a/core/jni/android_os_GraphicsEnvironment.cpp
+++ b/core/jni/android_os_GraphicsEnvironment.cpp
@@ -50,7 +50,7 @@
                                                     appPackageNameChars.c_str(), vulkanVersion);
 }
 
-void setAngleInfo_native(JNIEnv* env, jobject clazz, jstring path, jboolean useNativeDriver,
+void setAngleInfo_native(JNIEnv* env, jobject clazz, jstring path, jboolean useSystemAngle,
                          jstring packageName, jobjectArray featuresObj) {
     ScopedUtfChars pathChars(env, path);
     ScopedUtfChars packageNameChars(env, packageName);
@@ -73,7 +73,7 @@
         }
     }
 
-    android::GraphicsEnv::getInstance().setAngleInfo(pathChars.c_str(), useNativeDriver,
+    android::GraphicsEnv::getInstance().setAngleInfo(pathChars.c_str(), useSystemAngle,
                                                      packageNameChars.c_str(), features);
 }
 
@@ -118,7 +118,7 @@
          reinterpret_cast<void*>(setGpuStats_native)},
         {"setInjectLayersPrSetDumpable", "()Z",
          reinterpret_cast<void*>(setInjectLayersPrSetDumpable_native)},
-        {"nativeSetAngleInfo", "(Ljava/lang/String;ZLjava/lang/String;[Ljava/lang/String;)V",
+        {"setAngleInfo", "(Ljava/lang/String;ZLjava/lang/String;[Ljava/lang/String;)V",
          reinterpret_cast<void*>(setAngleInfo_native)},
         {"setLayerPaths", "(Ljava/lang/ClassLoader;Ljava/lang/String;)V",
          reinterpret_cast<void*>(setLayerPaths_native)},
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index b0afa68..66b9ade6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -821,13 +821,17 @@
      * @param interceptBack whether back should be intercepted or not.
      */
     void updateWindowFlagsForBackpress(boolean interceptBack) {
-        if (mStackView != null && mAddedToWindowManager) {
+        if (mAddedToWindowManager) {
             mWmLayoutParams.flags = interceptBack
                     ? 0
                     : WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                             | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
             mWmLayoutParams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
-            mWindowManager.updateViewLayout(mStackView, mWmLayoutParams);
+            if (mStackView != null) {
+                mWindowManager.updateViewLayout(mStackView, mWmLayoutParams);
+            } else if (mLayerView != null) {
+                mWindowManager.updateViewLayout(mLayerView, mWmLayoutParams);
+            }
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
index 76ca68b..517f9f2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -54,6 +54,15 @@
     public static final boolean IS_DISPLAY_CHANGE_ENABLED = SystemProperties.getBoolean(
             "persist.wm.debug.desktop_change_display", false);
 
+
+    /**
+     * Flag to indicate that desktop stashing is enabled.
+     * When enabled, swiping home from desktop stashes the open apps. Next app that launches,
+     * will be added to the desktop.
+     */
+    private static final boolean IS_STASHING_ENABLED = SystemProperties.getBoolean(
+            "persist.wm.debug.desktop_stashing", false);
+
     /**
      * Return {@code true} if desktop mode support is enabled
      */
@@ -84,6 +93,13 @@
     }
 
     /**
+     * Return {@code true} if desktop task stashing is enabled when going home.
+     * Allows users to use home screen to add tasks to desktop.
+     */
+    public static boolean isStashingEnabled() {
+        return IS_STASHING_ENABLED;
+    }
+    /**
      * Check if desktop mode is active
      *
      * @return {@code true} if active
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 9bc28a46..b15fd91 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
@@ -150,20 +150,24 @@
      * back to front during the launch.
      */
     fun stashDesktopApps(displayId: Int) {
-        KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: stashDesktopApps")
-        desktopModeTaskRepository.setStashed(displayId, true)
+        if (DesktopModeStatus.isStashingEnabled()) {
+            KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: stashDesktopApps")
+            desktopModeTaskRepository.setStashed(displayId, true)
+        }
     }
 
     /**
      * Clear the stashed state for the given display
      */
     fun hideStashedDesktopApps(displayId: Int) {
-        KtProtoLog.v(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksController: hideStashedApps displayId=%d",
-                displayId
-        )
-        desktopModeTaskRepository.setStashed(displayId, false)
+        if (DesktopModeStatus.isStashingEnabled()) {
+            KtProtoLog.v(
+                    WM_SHELL_DESKTOP_MODE,
+                    "DesktopTasksController: hideStashedApps displayId=%d",
+                    displayId
+            )
+            desktopModeTaskRepository.setStashed(displayId, false)
+        }
     }
 
     /** Get number of tasks that are marked as visible */
@@ -172,9 +176,13 @@
     }
 
     /** Move a task with given `taskId` to desktop */
-    fun moveToDesktop(taskId: Int, wct: WindowContainerTransaction = WindowContainerTransaction()) {
+    fun moveToDesktop(
+            decor: DesktopModeWindowDecoration,
+            taskId: Int,
+            wct: WindowContainerTransaction = WindowContainerTransaction()
+    ) {
         shellTaskOrganizer.getRunningTaskInfo(taskId)?.let {
-            task -> moveToDesktop(task, wct)
+            task -> moveToDesktop(decor, task, wct)
         }
     }
 
@@ -182,6 +190,7 @@
      * Move a task to desktop
      */
     fun moveToDesktop(
+            decor: DesktopModeWindowDecoration,
             task: RunningTaskInfo,
             wct: WindowContainerTransaction = WindowContainerTransaction()
     ) {
@@ -195,7 +204,7 @@
         addMoveToDesktopChanges(wct, task)
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
+            enterDesktopTaskTransitionHandler.moveToDesktop(wct, decor)
         } else {
             shellTaskOrganizer.applyTransaction(wct)
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
index 22929c76..16b2393 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
@@ -20,6 +20,7 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.RectEvaluator;
 import android.animation.ValueAnimator;
 import android.app.ActivityManager;
 import android.graphics.PointF;
@@ -36,6 +37,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration;
 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator;
 
 import java.util.ArrayList;
@@ -60,6 +62,7 @@
     private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
     private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback;
     private MoveToDesktopAnimator mMoveToDesktopAnimator;
+    private DesktopModeWindowDecoration mDesktopModeWindowDecoration;
 
     public EnterDesktopTaskTransitionHandler(
             Transitions transitions) {
@@ -128,6 +131,18 @@
                 onAnimationEndCallback);
     }
 
+    /**
+     * Starts Transition of type TRANSIT_MOVE_TO_DESKTOP
+     * @param wct WindowContainerTransaction for transition
+     * @param decor {@link DesktopModeWindowDecoration} of task being animated
+     */
+    public void moveToDesktop(@NonNull WindowContainerTransaction wct,
+            DesktopModeWindowDecoration decor) {
+        mDesktopModeWindowDecoration = decor;
+        startTransition(Transitions.TRANSIT_MOVE_TO_DESKTOP, wct,
+                null /* onAnimationEndCallback */);
+    }
+
     @Override
     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startT,
@@ -167,138 +182,209 @@
         }
 
         final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
-        if (type == Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE
+        if (type == Transitions.TRANSIT_MOVE_TO_DESKTOP
                 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
-            // Transitioning to freeform but keeping fullscreen bounds, so the crop is set
-            // to null and we don't require an animation
-            final SurfaceControl sc = change.getLeash();
-            startT.setWindowCrop(sc, null);
-
-            if (mMoveToDesktopAnimator == null
-                    || mMoveToDesktopAnimator.getTaskId() != change.getTaskInfo().taskId) {
-                Slog.e(TAG, "No animator available for this transition");
-                return false;
-            }
-
-            // Calculate and set position of the task
-            final PointF position = mMoveToDesktopAnimator.getPosition();
-            startT.setPosition(sc, position.x, position.y);
-            finishT.setPosition(sc, position.x, position.y);
-
-            startT.apply();
-
-            mTransitions.getMainExecutor().execute(
-                    () -> finishCallback.onTransitionFinished(null, null));
-
-            return true;
+            return animateMoveToDesktop(change, startT, finishCallback);
         }
 
-        Rect endBounds = change.getEndAbsBounds();
+        if (type == Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE
+                && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+            return animateStartDragToDesktopMode(change, startT, finishT, finishCallback);
+        }
+
+        final Rect endBounds = change.getEndAbsBounds();
         if (type == Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE
                 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
                 && !endBounds.isEmpty()) {
-            // This Transition animates a task to freeform bounds after being dragged into freeform
-            // mode and brings the remaining freeform tasks to front
-            final SurfaceControl sc = change.getLeash();
-            startT.setWindowCrop(sc, endBounds.width(),
-                    endBounds.height());
-            startT.apply();
-
-            // End the animation that shrinks the window when task is first dragged from fullscreen
-            if (mMoveToDesktopAnimator != null) {
-                mMoveToDesktopAnimator.endAnimator();
-            }
-
-            // We want to find the scale of the current bounds relative to the end bounds. The
-            // task is currently scaled to DRAG_FREEFORM_SCALE and the final bounds will be
-            // scaled to FINAL_FREEFORM_SCALE. So, it is scaled to
-            // DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE relative to the freeform bounds
-            final ValueAnimator animator =
-                    ValueAnimator.ofFloat(
-                            MoveToDesktopAnimator.DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE, 1f);
-            animator.setDuration(FREEFORM_ANIMATION_DURATION);
-            final SurfaceControl.Transaction t = mTransactionSupplier.get();
-            animator.addUpdateListener(animation -> {
-                final float animationValue = (float) animation.getAnimatedValue();
-                t.setScale(sc, animationValue, animationValue);
-
-                final float animationWidth = endBounds.width() * animationValue;
-                final float animationHeight = endBounds.height() * animationValue;
-                final int animationX = endBounds.centerX() - (int) (animationWidth / 2);
-                final int animationY = endBounds.centerY() - (int) (animationHeight / 2);
-
-                t.setPosition(sc, animationX, animationY);
-                t.apply();
-            });
-
-            animator.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    if (mOnAnimationFinishedCallback != null) {
-                        mOnAnimationFinishedCallback.accept(finishT);
-                    }
-                    mTransitions.getMainExecutor().execute(
-                            () -> finishCallback.onTransitionFinished(null, null));
-                }
-            });
-
-            animator.start();
-            return true;
+            return animateFinalizeDragToDesktopMode(change, startT, finishT, finishCallback,
+                    endBounds);
         }
 
         if (type == Transitions.TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE
                 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
-            // This Transition animates a task to fullscreen after being dragged from the status
-            // bar and then released back into the status bar area
-            final SurfaceControl sc = change.getLeash();
-            // Hide the first (fullscreen) frame because the animation will start from the smaller
-            // scale size.
-            startT.hide(sc)
-                    .setWindowCrop(sc, endBounds.width(), endBounds.height())
-                    .apply();
-
-            if (mMoveToDesktopAnimator == null
-                    || mMoveToDesktopAnimator.getTaskId() != change.getTaskInfo().taskId) {
-                Slog.e(TAG, "No animator available for this transition");
-                return false;
-            }
-
-            // End the animation that shrinks the window when task is first dragged from fullscreen
-            mMoveToDesktopAnimator.endAnimator();
-
-            final ValueAnimator animator = new ValueAnimator();
-            animator.setFloatValues(MoveToDesktopAnimator.DRAG_FREEFORM_SCALE, 1f);
-            animator.setDuration(FREEFORM_ANIMATION_DURATION);
-            final SurfaceControl.Transaction t = mTransactionSupplier.get();
-
-            // Get position of the task
-            final float x = mMoveToDesktopAnimator.getPosition().x;
-            final float y = mMoveToDesktopAnimator.getPosition().y;
-
-            animator.addUpdateListener(animation -> {
-                final float scale = (float) animation.getAnimatedValue();
-                t.setPosition(sc, x * (1 - scale), y * (1 - scale))
-                        .setScale(sc, scale, scale)
-                        .show(sc)
-                        .apply();
-            });
-            animator.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    if (mOnAnimationFinishedCallback != null) {
-                        mOnAnimationFinishedCallback.accept(finishT);
-                    }
-                    mTransitions.getMainExecutor().execute(
-                            () -> finishCallback.onTransitionFinished(null, null));
-                }
-            });
-            animator.start();
-            return true;
+            return animateCancelDragToDesktopMode(change, startT, finishT, finishCallback,
+                    endBounds);
         }
 
         return false;
     }
 
+    private boolean animateMoveToDesktop(
+            @NonNull TransitionInfo.Change change,
+            @NonNull SurfaceControl.Transaction startT,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        if (mDesktopModeWindowDecoration == null) {
+            Slog.e(TAG, "Window Decoration is not available for this transition");
+            return false;
+        }
+
+        final SurfaceControl leash = change.getLeash();
+        final Rect startBounds = change.getStartAbsBounds();
+        startT.setPosition(leash, startBounds.left, startBounds.right)
+                .setWindowCrop(leash, startBounds.width(), startBounds.height())
+                .show(leash);
+        mDesktopModeWindowDecoration.showResizeVeil(startT, startBounds);
+
+        final ValueAnimator animator = ValueAnimator.ofObject(new RectEvaluator(),
+                change.getStartAbsBounds(), change.getEndAbsBounds());
+        animator.setDuration(FREEFORM_ANIMATION_DURATION);
+        SurfaceControl.Transaction t = mTransactionSupplier.get();
+        animator.addUpdateListener(animation -> {
+            final Rect animationValue = (Rect) animator.getAnimatedValue();
+            t.setPosition(leash, animationValue.left, animationValue.right)
+                    .setWindowCrop(leash, animationValue.width(), animationValue.height())
+                    .show(leash);
+            mDesktopModeWindowDecoration.updateResizeVeil(t, animationValue);
+        });
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mDesktopModeWindowDecoration.hideResizeVeil();
+                mTransitions.getMainExecutor().execute(
+                        () -> finishCallback.onTransitionFinished(null, null));
+            }
+        });
+        animator.start();
+        return true;
+    }
+
+    private boolean animateStartDragToDesktopMode(
+            @NonNull TransitionInfo.Change change,
+            @NonNull SurfaceControl.Transaction startT,
+            @NonNull SurfaceControl.Transaction finishT,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        // Transitioning to freeform but keeping fullscreen bounds, so the crop is set
+        // to null and we don't require an animation
+        final SurfaceControl sc = change.getLeash();
+        startT.setWindowCrop(sc, null);
+
+        if (mMoveToDesktopAnimator == null
+                || mMoveToDesktopAnimator.getTaskId() != change.getTaskInfo().taskId) {
+            Slog.e(TAG, "No animator available for this transition");
+            return false;
+        }
+
+        // Calculate and set position of the task
+        final PointF position = mMoveToDesktopAnimator.getPosition();
+        startT.setPosition(sc, position.x, position.y);
+        finishT.setPosition(sc, position.x, position.y);
+
+        startT.apply();
+
+        mTransitions.getMainExecutor().execute(
+                () -> finishCallback.onTransitionFinished(null, null));
+
+        return true;
+    }
+
+    private boolean animateFinalizeDragToDesktopMode(
+            @NonNull TransitionInfo.Change change,
+            @NonNull SurfaceControl.Transaction startT,
+            @NonNull SurfaceControl.Transaction finishT,
+            @NonNull Transitions.TransitionFinishCallback finishCallback,
+            @NonNull Rect endBounds) {
+        // This Transition animates a task to freeform bounds after being dragged into freeform
+        // mode and brings the remaining freeform tasks to front
+        final SurfaceControl sc = change.getLeash();
+        startT.setWindowCrop(sc, endBounds.width(),
+                endBounds.height());
+        startT.apply();
+
+        // End the animation that shrinks the window when task is first dragged from fullscreen
+        if (mMoveToDesktopAnimator != null) {
+            mMoveToDesktopAnimator.endAnimator();
+        }
+
+        // We want to find the scale of the current bounds relative to the end bounds. The
+        // task is currently scaled to DRAG_FREEFORM_SCALE and the final bounds will be
+        // scaled to FINAL_FREEFORM_SCALE. So, it is scaled to
+        // DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE relative to the freeform bounds
+        final ValueAnimator animator =
+                ValueAnimator.ofFloat(
+                        MoveToDesktopAnimator.DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE, 1f);
+        animator.setDuration(FREEFORM_ANIMATION_DURATION);
+        final SurfaceControl.Transaction t = mTransactionSupplier.get();
+        animator.addUpdateListener(animation -> {
+            final float animationValue = (float) animation.getAnimatedValue();
+            t.setScale(sc, animationValue, animationValue);
+
+            final float animationWidth = endBounds.width() * animationValue;
+            final float animationHeight = endBounds.height() * animationValue;
+            final int animationX = endBounds.centerX() - (int) (animationWidth / 2);
+            final int animationY = endBounds.centerY() - (int) (animationHeight / 2);
+
+            t.setPosition(sc, animationX, animationY);
+            t.apply();
+        });
+
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mOnAnimationFinishedCallback != null) {
+                    mOnAnimationFinishedCallback.accept(finishT);
+                }
+                mTransitions.getMainExecutor().execute(
+                        () -> finishCallback.onTransitionFinished(null, null));
+            }
+        });
+
+        animator.start();
+        return true;
+    }
+    private boolean animateCancelDragToDesktopMode(
+            @NonNull TransitionInfo.Change change,
+            @NonNull SurfaceControl.Transaction startT,
+            @NonNull SurfaceControl.Transaction finishT,
+            @NonNull Transitions.TransitionFinishCallback finishCallback,
+            @NonNull Rect endBounds) {
+        // This Transition animates a task to fullscreen after being dragged from the status
+        // bar and then released back into the status bar area
+        final SurfaceControl sc = change.getLeash();
+        // Hide the first (fullscreen) frame because the animation will start from the smaller
+        // scale size.
+        startT.hide(sc)
+                .setWindowCrop(sc, endBounds.width(), endBounds.height())
+                .apply();
+
+        if (mMoveToDesktopAnimator == null
+                || mMoveToDesktopAnimator.getTaskId() != change.getTaskInfo().taskId) {
+            Slog.e(TAG, "No animator available for this transition");
+            return false;
+        }
+
+        // End the animation that shrinks the window when task is first dragged from fullscreen
+        mMoveToDesktopAnimator.endAnimator();
+
+        final ValueAnimator animator = new ValueAnimator();
+        animator.setFloatValues(MoveToDesktopAnimator.DRAG_FREEFORM_SCALE, 1f);
+        animator.setDuration(FREEFORM_ANIMATION_DURATION);
+        final SurfaceControl.Transaction t = mTransactionSupplier.get();
+
+        // Get position of the task
+        final float x = mMoveToDesktopAnimator.getPosition().x;
+        final float y = mMoveToDesktopAnimator.getPosition().y;
+
+        animator.addUpdateListener(animation -> {
+            final float scale = (float) animation.getAnimatedValue();
+            t.setPosition(sc, x * (1 - scale), y * (1 - scale))
+                    .setScale(sc, scale, scale)
+                    .show(sc)
+                    .apply();
+        });
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mOnAnimationFinishedCallback != null) {
+                    mOnAnimationFinishedCallback.accept(finishT);
+                }
+                mTransitions.getMainExecutor().execute(
+                        () -> finishCallback.onTransitionFinished(null, null));
+            }
+        });
+        animator.start();
+        return true;
+    }
+
     @Nullable
     @Override
     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index e45dacf..e2dce88 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -168,6 +168,9 @@
     public static final int TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE =
             WindowManager.TRANSIT_FIRST_CUSTOM + 14;
 
+    /** Transition to animate task to desktop. */
+    public static final int TRANSIT_MOVE_TO_DESKTOP = WindowManager.TRANSIT_FIRST_CUSTOM + 15;
+
     private final WindowOrganizer mOrganizer;
     private final Context mContext;
     private final ShellExecutor mMainExecutor;
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 ea9976d..4cc755b 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
@@ -222,7 +222,8 @@
                 && (info.getType() == Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE
                 || info.getType() == Transitions.TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE
                 || info.getType() == Transitions.TRANSIT_EXIT_DESKTOP_MODE
-                || info.getType() == Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE)) {
+                || info.getType() == Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE
+                || info.getType() == Transitions.TRANSIT_MOVE_TO_DESKTOP)) {
             mWindowDecorByTaskId.get(change.getTaskInfo().taskId)
                     .addTransitionPausingRelayout(transition);
         }
@@ -356,7 +357,8 @@
                     // App sometimes draws before the insets from WindowDecoration#relayout have
                     // been added, so they must be added here
                     mWindowDecorByTaskId.get(mTaskId).addCaptionInset(wct);
-                    mDesktopTasksController.get().moveToDesktop(mTaskId, wct);
+                    decoration.incrementRelayoutBlock();
+                    mDesktopTasksController.get().moveToDesktop(decoration, mTaskId, wct);
                 }
                 decoration.closeHandleMenu();
             } else if (id == R.id.fullscreen_button) {
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 1477cf7..5d87cf8 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
@@ -57,6 +57,7 @@
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.After
@@ -92,6 +93,7 @@
     @Mock lateinit var mToggleResizeDesktopTaskTransitionHandler:
             ToggleResizeDesktopTaskTransitionHandler
     @Mock lateinit var launchAdjacentController: LaunchAdjacentController
+    @Mock lateinit var desktopModeWindowDecoration: DesktopModeWindowDecoration
 
     private lateinit var mockitoSession: StaticMockitoSession
     private lateinit var controller: DesktopTasksController
@@ -276,8 +278,8 @@
     fun moveToDesktop_displayFullscreen_windowingModeSetToFreeform() {
         val task = setUpFullscreenTask()
         task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN
-        controller.moveToDesktop(task)
-        val wct = getLatestWct(expectTransition = TRANSIT_CHANGE)
+        controller.moveToDesktop(desktopModeWindowDecoration, task)
+        val wct = getLatestMoveToDesktopWct()
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
             .isEqualTo(WINDOWING_MODE_FREEFORM)
     }
@@ -286,15 +288,15 @@
     fun moveToDesktop_displayFreeform_windowingModeSetToUndefined() {
         val task = setUpFullscreenTask()
         task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM
-        controller.moveToDesktop(task)
-        val wct = getLatestWct(expectTransition = TRANSIT_CHANGE)
+        controller.moveToDesktop(desktopModeWindowDecoration, task)
+        val wct = getLatestMoveToDesktopWct()
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
                 .isEqualTo(WINDOWING_MODE_UNDEFINED)
     }
 
     @Test
     fun moveToDesktop_nonExistentTask_doesNothing() {
-        controller.moveToDesktop(999)
+        controller.moveToDesktop(desktopModeWindowDecoration, 999)
         verifyWCTNotExecuted()
     }
 
@@ -305,9 +307,9 @@
         val fullscreenTask = setUpFullscreenTask()
         markTaskHidden(freeformTask)
 
-        controller.moveToDesktop(fullscreenTask)
+        controller.moveToDesktop(desktopModeWindowDecoration, fullscreenTask)
 
-        with(getLatestWct(expectTransition = TRANSIT_CHANGE)) {
+        with(getLatestMoveToDesktopWct()) {
             // Operations should include home task, freeform task
             assertThat(hierarchyOps).hasSize(3)
             assertReorderSequence(homeTask, freeformTask, fullscreenTask)
@@ -327,9 +329,9 @@
         val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY)
         markTaskHidden(freeformTaskSecond)
 
-        controller.moveToDesktop(fullscreenTaskDefault)
+        controller.moveToDesktop(desktopModeWindowDecoration, fullscreenTaskDefault)
 
-        with(getLatestWct(expectTransition = TRANSIT_CHANGE)) {
+        with(getLatestMoveToDesktopWct()) {
             // Check that hierarchy operations do not include tasks from second display
             assertThat(hierarchyOps.map { it.container })
                 .doesNotContain(homeTaskSecond.token.asBinder())
@@ -498,6 +500,7 @@
     @Test
     fun handleRequest_fullscreenTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() {
         assumeTrue(ENABLE_SHELL_TRANSITIONS)
+        whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
 
         val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY)
         markTaskHidden(stashedFreeformTask)
@@ -569,6 +572,7 @@
     @Test
     fun handleRequest_freeformTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() {
         assumeTrue(ENABLE_SHELL_TRANSITIONS)
+        whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
 
         val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY)
         markTaskHidden(stashedFreeformTask)
@@ -626,6 +630,8 @@
 
     @Test
     fun stashDesktopApps_stateUpdates() {
+        whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
+
         controller.stashDesktopApps(DEFAULT_DISPLAY)
 
         assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isTrue()
@@ -634,6 +640,8 @@
 
     @Test
     fun hideStashedDesktopApps_stateUpdates() {
+        whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
+
         desktopModeTaskRepository.setStashed(DEFAULT_DISPLAY, true)
         desktopModeTaskRepository.setStashed(SECOND_DISPLAY, true)
         controller.hideStashedDesktopApps(DEFAULT_DISPLAY)
@@ -715,6 +723,16 @@
         return arg.value
     }
 
+    private fun getLatestMoveToDesktopWct(): WindowContainerTransaction {
+        val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        if (ENABLE_SHELL_TRANSITIONS) {
+            verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any())
+        } else {
+            verify(shellTaskOrganizer).applyTransaction(arg.capture())
+        }
+        return arg.value
+    }
+
     private fun verifyWCTNotExecuted() {
         if (ENABLE_SHELL_TRANSITIONS) {
             verify(transitions, never()).startTransition(anyInt(), any(), isNull())
diff --git a/services/tests/servicestests/src/com/android/server/companion/utils/OWNERS b/services/tests/servicestests/src/com/android/server/companion/utils/OWNERS
new file mode 100644
index 0000000..008a53f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/companion/utils/OWNERS
@@ -0,0 +1 @@
+include /services/companion/java/com/android/server/companion/OWNERS
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/companion/utils/PackageUtilsTest.java b/services/tests/servicestests/src/com/android/server/companion/utils/PackageUtilsTest.java
new file mode 100644
index 0000000..01159b1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/companion/utils/PackageUtilsTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2023 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.companion.utils;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.Signature;
+import android.content.pm.SigningDetails;
+import android.content.res.Resources;
+import android.platform.test.annotations.Presubmit;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.companion.PackageUtils;
+import com.android.server.pm.pkg.AndroidPackage;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Presubmit
+@RunWith(AndroidTestingRunner.class)
+public class PackageUtilsTest {
+    private static final String[] ALLOWED_PACKAGE_NAMES = new String[]{
+            "allowed_app",
+    };
+    private static final Signature[] ALLOWED_PACKAGE_SIGNATURES = new Signature[]{
+            new Signature("001122"),
+    };
+    private static final String[] DISALLOWED_PACKAGE_NAMES = new String[]{
+            "disallowed_app",
+    };
+    private static final Signature[] DISALLOWED_PACKAGE_SIGNATURES = new Signature[]{
+            new Signature("778899"),
+    };
+
+    @Test
+    public void isAllowlisted_true() {
+        Context context = spy(
+                new ContextWrapper(
+                        InstrumentationRegistry.getInstrumentation().getTargetContext()));
+        final Resources res = spy(context.getResources());
+        doReturn(ALLOWED_PACKAGE_NAMES).when(res).getStringArray(
+                com.android.internal.R.array.config_companionDevicePackages);
+        doReturn(android.util.PackageUtils.computeSignaturesSha256Digests(
+                ALLOWED_PACKAGE_SIGNATURES)).when(res).getStringArray(
+                com.android.internal.R.array.config_companionDeviceCerts);
+        doReturn(res).when(context).getResources();
+        PackageManagerInternal pm = mock(PackageManagerInternal.class);
+        AndroidPackage ap = mock(AndroidPackage.class);
+        SigningDetails sd = new SigningDetails(
+                ALLOWED_PACKAGE_SIGNATURES,
+                SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3,
+                null,
+                null);
+        doReturn(ap).when(pm).getPackage(ALLOWED_PACKAGE_NAMES[0]);
+        doReturn(sd).when(ap).getSigningDetails();
+        assertTrue(PackageUtils.isPackageAllowlisted(context, pm, ALLOWED_PACKAGE_NAMES[0]));
+    }
+
+    @Test
+    public void isAllowlisted_package_disallowed() {
+        Context context = spy(
+                new ContextWrapper(
+                        InstrumentationRegistry.getInstrumentation().getTargetContext()));
+        final Resources res = spy(context.getResources());
+        doReturn(ALLOWED_PACKAGE_NAMES).when(res).getStringArray(
+                com.android.internal.R.array.config_companionDevicePackages);
+        doReturn(android.util.PackageUtils.computeSignaturesSha256Digests(
+                ALLOWED_PACKAGE_SIGNATURES)).when(res).getStringArray(
+                com.android.internal.R.array.config_companionDeviceCerts);
+        doReturn(res).when(context).getResources();
+        PackageManagerInternal pm = mock(PackageManagerInternal.class);
+        AndroidPackage ap = mock(AndroidPackage.class);
+        SigningDetails sd = new SigningDetails(
+                ALLOWED_PACKAGE_SIGNATURES, // Giving the package a wrong signature
+                SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3,
+                null,
+                null);
+        doReturn(ap).when(pm).getPackage(DISALLOWED_PACKAGE_NAMES[0]);
+        doReturn(sd).when(ap).getSigningDetails();
+        assertFalse(PackageUtils.isPackageAllowlisted(context, pm, DISALLOWED_PACKAGE_NAMES[0]));
+    }
+
+    @Test
+    public void isAllowlisted_signature_mismatch() {
+        Context context = spy(
+                new ContextWrapper(
+                        InstrumentationRegistry.getInstrumentation().getTargetContext()));
+        final Resources res = spy(context.getResources());
+        doReturn(ALLOWED_PACKAGE_NAMES).when(res).getStringArray(
+                com.android.internal.R.array.config_companionDevicePackages);
+        doReturn(android.util.PackageUtils.computeSignaturesSha256Digests(
+                ALLOWED_PACKAGE_SIGNATURES)).when(res).getStringArray(
+                com.android.internal.R.array.config_companionDeviceCerts);
+        doReturn(res).when(context).getResources();
+        PackageManagerInternal pm = mock(PackageManagerInternal.class);
+        AndroidPackage ap = mock(AndroidPackage.class);
+        SigningDetails sd = new SigningDetails(
+                DISALLOWED_PACKAGE_SIGNATURES, // Giving the package a wrong signature
+                SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3,
+                null,
+                null);
+        doReturn(ap).when(pm).getPackage(ALLOWED_PACKAGE_NAMES[0]);
+        doReturn(sd).when(ap).getSigningDetails();
+        assertFalse(PackageUtils.isPackageAllowlisted(context, pm, ALLOWED_PACKAGE_NAMES[0]));
+    }
+}
diff --git a/tools/aapt2/util/Util.cpp b/tools/aapt2/util/Util.cpp
index be87766..3d83caf2 100644
--- a/tools/aapt2/util/Util.cpp
+++ b/tools/aapt2/util/Util.cpp
@@ -21,6 +21,7 @@
 #include <string>
 #include <vector>
 
+#include "android-base/parseint.h"
 #include "android-base/stringprintf.h"
 #include "android-base/strings.h"
 #include "androidfw/BigBuffer.h"
@@ -229,14 +230,29 @@
   static const char* const sMinorVersion = "19";
 
   // The build id of aapt2 binary.
-  static std::string sBuildId = android::build::GetBuildNumber();
+  static const std::string sBuildId = [] {
+    std::string buildNumber = android::build::GetBuildNumber();
 
-  if (android::base::StartsWith(sBuildId, "eng.")) {
-    time_t now = time(0);
-    tm* ltm = localtime(&now);
+    if (android::base::StartsWith(buildNumber, "eng.")) {
+      // android::build::GetBuildNumber() returns something like "eng.user.20230725.214219" where
+      // the latter two parts are "yyyyMMdd.HHmmss" at build time. Use "yyyyMM" in the fingerprint.
+      std::vector<std::string> parts = util::Split(buildNumber, '.');
+      int buildYear;
+      int buildMonth;
+      if (parts.size() < 3 || parts[2].length() < 6 ||
+          !android::base::ParseInt(parts[2].substr(0, 4), &buildYear) ||
+          !android::base::ParseInt(parts[2].substr(4, 2), &buildMonth)) {
+        // Fallback to localtime() if GetBuildNumber() returns an unexpected output.
+        time_t now = time(0);
+        tm* ltm = localtime(&now);
+        buildYear = 1900 + ltm->tm_year;
+        buildMonth = 1 + ltm->tm_mon;
+      }
 
-    sBuildId = android::base::StringPrintf("eng.%d%d", 1900 + ltm->tm_year, 1 + ltm->tm_mon);
-  }
+      buildNumber = android::base::StringPrintf("eng.%04d%02d", buildYear, buildMonth);
+    }
+    return buildNumber;
+  }();
 
   return android::base::StringPrintf("%s.%s-%s", sMajorVersion, sMinorVersion, sBuildId.c_str());
 }