Merge "Fix AppStorageSizeTest"
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
index d5c3de1..b478a379 100644
--- a/core/java/android/os/FileUtils.java
+++ b/core/java/android/os/FileUtils.java
@@ -84,7 +84,6 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Comparator;
-import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
@@ -1314,31 +1313,31 @@
     private static long toBytes(long value, String unit) {
         unit = unit.toUpperCase();
 
-        if (List.of("B").contains(unit)) {
+        if ("B".equals(unit)) {
             return value;
         }
 
-        if (List.of("K", "KB").contains(unit)) {
+        if ("K".equals(unit) || "KB".equals(unit)) {
             return DataUnit.KILOBYTES.toBytes(value);
         }
 
-        if (List.of("M", "MB").contains(unit)) {
+        if ("M".equals(unit) || "MB".equals(unit)) {
             return DataUnit.MEGABYTES.toBytes(value);
         }
 
-        if (List.of("G", "GB").contains(unit)) {
+        if ("G".equals(unit) || "GB".equals(unit)) {
             return DataUnit.GIGABYTES.toBytes(value);
         }
 
-        if (List.of("KI", "KIB").contains(unit)) {
+        if ("KI".equals(unit) || "KIB".equals(unit)) {
             return DataUnit.KIBIBYTES.toBytes(value);
         }
 
-        if (List.of("MI", "MIB").contains(unit)) {
+        if ("MI".equals(unit) || "MIB".equals(unit)) {
             return DataUnit.MEBIBYTES.toBytes(value);
         }
 
-        if (List.of("GI", "GIB").contains(unit)) {
+        if ("GI".equals(unit) || "GIB".equals(unit)) {
             return DataUnit.GIBIBYTES.toBytes(value);
         }
 
@@ -1370,7 +1369,7 @@
                 sign = -1;
             }
 
-            fmtSize = fmtSize.replace(first + "", "");
+            fmtSize = fmtSize.substring(1);
         }
 
         int index = 0;
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ad39c0c..336f183 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -18674,6 +18674,9 @@
     /**
      * Activity Action: For system or preinstalled apps to show their {@link Activity} embedded
      * in Settings app on large screen devices.
+     *
+     * Developers should resolve the Intent action before using it.
+     *
      * <p>
      *     Input: {@link #EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI} must be included to
      * specify the intent for the activity which will be embedded in Settings app.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index b6327e5..d9eaeee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -38,7 +38,6 @@
 import android.provider.Settings.Global;
 import android.util.Log;
 import android.util.SparseArray;
-import android.view.IRemoteAnimationFinishedCallback;
 import android.view.IRemoteAnimationRunner;
 import android.view.IWindowFocusObserver;
 import android.view.InputDevice;
@@ -69,7 +68,7 @@
  * Controls the window animation run when a user initiates a back gesture.
  */
 public class BackAnimationController implements RemoteCallable<BackAnimationController> {
-    private static final String TAG = "BackAnimationController";
+    private static final String TAG = "ShellBackPreview";
     private static final int SETTING_VALUE_OFF = 0;
     private static final int SETTING_VALUE_ON = 1;
     public static final boolean IS_ENABLED =
@@ -82,16 +81,15 @@
     /** Predictive back animation developer option */
     private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
     /**
-     * Max duration to wait for a transition to finish before accepting another gesture start
-     * request.
+     * Max duration to wait for an animation to finish before triggering the real back.
      */
-    private static final long MAX_TRANSITION_DURATION = 2000;
+    private static final long MAX_ANIMATION_DURATION = 2000;
 
     /** True when a back gesture is ongoing */
     private boolean mBackGestureStarted = false;
 
-    /** Tracks if an uninterruptible transition is in progress */
-    private boolean mTransitionInProgress = false;
+    /** Tracks if an uninterruptible animation is in progress */
+    private boolean mPostCommitAnimationInProgress = false;
     /** Tracks if we should start the back gesture on the next motion move event */
     private boolean mShouldStartOnNextMoveEvent = false;
     /** @see #setTriggerBack(boolean) */
@@ -105,9 +103,9 @@
     private final ShellController mShellController;
     private final ShellExecutor mShellExecutor;
     private final Handler mBgHandler;
-    private final Runnable mResetTransitionRunnable = () -> {
-        ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Transition didn't finish in %d ms. Resetting...",
-                MAX_TRANSITION_DURATION);
+    private final Runnable mAnimationTimeoutRunnable = () -> {
+        ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...",
+                MAX_ANIMATION_DURATION);
         onBackAnimationFinished();
     };
 
@@ -119,6 +117,8 @@
 
     private final SparseArray<BackAnimationRunner> mAnimationDefinition = new SparseArray<>();
 
+    private IOnBackInvokedCallback mActiveCallback;
+
     @VisibleForTesting
     final IWindowFocusObserver mFocusObserver = new IWindowFocusObserver.Stub() {
         @Override
@@ -126,9 +126,9 @@
         @Override
         public void focusLost(IBinder inputToken) {
             mShellExecutor.execute(() -> {
-                if (!mBackGestureStarted || mTransitionInProgress) {
-                    // If an uninterruptible transition is already in progress, we should ignore
-                    // this due to the transition may cause focus lost. (alpha = 0)
+                if (!mBackGestureStarted || mPostCommitAnimationInProgress) {
+                    // If an uninterruptible animation is already in progress, we should ignore
+                    // this due to it may cause focus lost. (alpha = 0)
                     return;
                 }
                 ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Target window lost focus.");
@@ -180,26 +180,11 @@
     }
 
     private void initBackAnimationRunners() {
-        final IOnBackInvokedCallback dummyCallback = new IOnBackInvokedCallback.Default();
-        final IRemoteAnimationRunner dummyRunner = new IRemoteAnimationRunner.Default() {
-            @Override
-            public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
-                    RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
-                    IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
-                // Animation missing. Simply finish animation.
-                finishedCallback.onAnimationFinished();
-            }
-        };
-
-        final BackAnimationRunner dummyBackRunner =
-                new BackAnimationRunner(dummyCallback, dummyRunner);
         final CrossTaskBackAnimation crossTaskAnimation = new CrossTaskBackAnimation(mContext);
         mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_TASK,
                 new BackAnimationRunner(crossTaskAnimation.mCallback, crossTaskAnimation.mRunner));
         // TODO (238474994): register cross activity animation when it's completed.
-        mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY, dummyBackRunner);
         // TODO (236760237): register dialog close animation when it's completed.
-        mAnimationDefinition.set(BackNavigationInfo.TYPE_DIALOG_CLOSE, dummyBackRunner);
     }
 
     private void setupAnimationDeveloperSettingsObserver(
@@ -222,10 +207,9 @@
     private void updateEnableAnimationFromSetting() {
         int settingValue = Global.getInt(mContext.getContentResolver(),
                 Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF);
-        boolean isEnabled = settingValue == SETTING_VALUE_ON;
+        boolean isEnabled = settingValue == SETTING_VALUE_ON && IS_U_ANIMATION_ENABLED;
         mEnableAnimations.set(isEnabled);
-        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s",
-                isEnabled);
+        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled);
     }
 
     public BackAnimation getBackAnimationImpl() {
@@ -279,7 +263,9 @@
         public void setBackToLauncherCallback(IOnBackInvokedCallback callback,
                 IRemoteAnimationRunner runner) {
             executeRemoteCallWithTaskPermission(mController, "setBackToLauncherCallback",
-                    (controller) -> controller.setBackToLauncherCallback(callback, runner));
+                    (controller) -> controller.registerAnimation(
+                            BackNavigationInfo.TYPE_RETURN_TO_HOME,
+                            new BackAnimationRunner(callback, runner)));
         }
 
         @Override
@@ -294,44 +280,22 @@
         }
     }
 
-    @VisibleForTesting
-    void setBackToLauncherCallback(IOnBackInvokedCallback callback, IRemoteAnimationRunner runner) {
-        mAnimationDefinition.set(BackNavigationInfo.TYPE_RETURN_TO_HOME,
-                new BackAnimationRunner(callback, runner));
+    void registerAnimation(@BackNavigationInfo.BackTargetType int type,
+            @NonNull BackAnimationRunner runner) {
+        mAnimationDefinition.set(type, runner);
     }
 
     private void clearBackToLauncherCallback() {
         mAnimationDefinition.remove(BackNavigationInfo.TYPE_RETURN_TO_HOME);
     }
 
-    @VisibleForTesting
-    void onBackAnimationFinished() {
-        if (!mTransitionInProgress) {
-            return;
-        }
-
-        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()");
-
-        // Trigger real back.
-        if (mBackNavigationInfo != null) {
-            IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
-            if (mTriggerBack) {
-                dispatchOnBackInvoked(callback);
-            } else {
-                dispatchOnBackCancelled(callback);
-            }
-        }
-
-        finishBackNavigation();
-    }
-
     /**
      * Called when a new motion event needs to be transferred to this
      * {@link BackAnimationController}
      */
     public void onMotionEvent(float touchX, float touchY, int keyAction,
             @BackEvent.SwipeEdge int swipeEdge) {
-        if (mTransitionInProgress) {
+        if (mPostCommitAnimationInProgress) {
             return;
         }
 
@@ -348,7 +312,7 @@
                 onGestureStarted(touchX, touchY, swipeEdge);
                 mShouldStartOnNextMoveEvent = false;
             }
-            onMove(touchX, touchY, swipeEdge);
+            onMove();
         } else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) {
             ProtoLog.d(WM_SHELL_BACK_PREVIEW,
                     "Finishing gesture with event action: %d", keyAction);
@@ -386,30 +350,22 @@
             return;
         }
         final int backType = backNavigationInfo.getType();
-        final IOnBackInvokedCallback targetCallback;
         final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(backType);
         if (shouldDispatchToAnimator) {
+            mActiveCallback = mAnimationDefinition.get(backType).getCallback();
             mAnimationDefinition.get(backType).startGesture();
         } else {
-            targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
-            dispatchOnBackStarted(targetCallback, mTouchTracker.createStartEvent(null));
+            mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
+            dispatchOnBackStarted(mActiveCallback, mTouchTracker.createStartEvent(null));
         }
     }
 
-    private void onMove(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
+    private void onMove() {
         if (!mBackGestureStarted || mBackNavigationInfo == null || !mEnableAnimations.get()) {
             return;
         }
         final BackEvent backEvent = mTouchTracker.createProgressEvent();
-
-        int backType = mBackNavigationInfo.getType();
-        IOnBackInvokedCallback targetCallback;
-        if (shouldDispatchToAnimator(backType)) {
-            targetCallback = mAnimationDefinition.get(backType).getCallback();
-        } else {
-            targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
-        }
-        dispatchOnBackProgressed(targetCallback, backEvent);
+        dispatchOnBackProgressed(mActiveCallback, backEvent);
     }
 
     private void injectBackKey() {
@@ -431,57 +387,6 @@
         }
     }
 
-    private void onGestureFinished(boolean fromTouch) {
-        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
-        if (!mBackGestureStarted) {
-            finishBackNavigation();
-            return;
-        }
-
-        if (fromTouch) {
-            // Let touch reset the flag otherwise it will start a new back navigation and refresh
-            // the info when received a new move event.
-            mBackGestureStarted = false;
-        }
-
-        if (mTransitionInProgress) {
-            return;
-        }
-
-        if (mBackNavigationInfo == null) {
-            // No focus window found or core are running recents animation, inject back key as
-            // legacy behavior.
-            if (mTriggerBack) {
-                injectBackKey();
-            }
-            finishBackNavigation();
-            return;
-        }
-
-        int backType = mBackNavigationInfo.getType();
-        boolean shouldDispatchToAnimator = shouldDispatchToAnimator(backType);
-        final BackAnimationRunner runner = mAnimationDefinition.get(backType);
-        IOnBackInvokedCallback targetCallback = shouldDispatchToAnimator
-                ? runner.getCallback() : mBackNavigationInfo.getOnBackInvokedCallback();
-
-        if (shouldDispatchToAnimator) {
-            if (runner.onGestureFinished(mTriggerBack)) {
-                Log.w(TAG, "Gesture released, but animation didn't ready.");
-                return;
-            }
-            startTransition();
-        }
-        if (mTriggerBack) {
-            dispatchOnBackInvoked(targetCallback);
-        } else {
-            dispatchOnBackCancelled(targetCallback);
-        }
-        if (!shouldDispatchToAnimator) {
-            // Animation callback missing. Simply finish animation.
-            finishBackNavigation();
-        }
-    }
-
     private boolean shouldDispatchToAnimator(int backType) {
         return mEnableAnimations.get()
                 && mBackNavigationInfo != null
@@ -495,7 +400,7 @@
             return;
         }
         try {
-            if (shouldDispatchAnimation(callback)) {
+            if (mEnableAnimations.get()) {
                 callback.onBackStarted(backEvent);
             }
         } catch (RemoteException e) {
@@ -519,7 +424,7 @@
             return;
         }
         try {
-            if (shouldDispatchAnimation(callback)) {
+            if (mEnableAnimations.get()) {
                 callback.onBackCancelled();
             }
         } catch (RemoteException e) {
@@ -533,7 +438,7 @@
             return;
         }
         try {
-            if (shouldDispatchAnimation(callback)) {
+            if (mEnableAnimations.get()) {
                 callback.onBackProgressed(backEvent);
             }
         } catch (RemoteException e) {
@@ -541,17 +446,11 @@
         }
     }
 
-    private boolean shouldDispatchAnimation(IOnBackInvokedCallback callback) {
-        return (IS_U_ANIMATION_ENABLED || callback == mAnimationDefinition.get(
-                BackNavigationInfo.TYPE_RETURN_TO_HOME).getCallback())
-                && mEnableAnimations.get();
-    }
-
     /**
      * Sets to true when the back gesture has passed the triggering threshold, false otherwise.
      */
     public void setTriggerBack(boolean triggerBack) {
-        if (mTransitionInProgress) {
+        if (mPostCommitAnimationInProgress) {
             return;
         }
         mTriggerBack = triggerBack;
@@ -562,6 +461,109 @@
         mTouchTracker.setProgressThreshold(progressThreshold);
     }
 
+    /**
+     * Called when the gesture is released, then it could start the post commit animation.
+     */
+    private void onGestureFinished(boolean fromTouch) {
+        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
+        if (!mBackGestureStarted) {
+            finishBackNavigation();
+            return;
+        }
+
+        if (fromTouch) {
+            // Let touch reset the flag otherwise it will start a new back navigation and refresh
+            // the info when received a new move event.
+            mBackGestureStarted = false;
+        }
+
+        if (mPostCommitAnimationInProgress) {
+            ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation is still running");
+            return;
+        }
+
+        if (mBackNavigationInfo == null) {
+            // No focus window found or core are running recents animation, inject back key as
+            // legacy behavior.
+            if (mTriggerBack) {
+                injectBackKey();
+            }
+            finishBackNavigation();
+            return;
+        }
+
+        final int backType = mBackNavigationInfo.getType();
+        // Directly finish back navigation if no animator defined.
+        if (!shouldDispatchToAnimator(backType)) {
+            if (mTriggerBack) {
+                dispatchOnBackInvoked(mActiveCallback);
+            } else {
+                dispatchOnBackCancelled(mActiveCallback);
+            }
+            // Animation missing. Simply finish back navigation.
+            finishBackNavigation();
+            return;
+        }
+
+        final BackAnimationRunner runner = mAnimationDefinition.get(backType);
+        if (runner.isWaitingAnimation()) {
+            ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Gesture released, but animation didn't ready.");
+            return;
+        }
+        startPostCommitAnimation();
+    }
+
+    /**
+     * Start the phase 2 animation when gesture is released.
+     * Callback to {@link #onBackAnimationFinished} when it is finished or timeout.
+     */
+    private void startPostCommitAnimation() {
+        if (mPostCommitAnimationInProgress) {
+            return;
+        }
+        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startPostCommitAnimation()");
+        mPostCommitAnimationInProgress = true;
+        mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION);
+
+        // The next callback should be {@link #onBackAnimationFinished}.
+        if (mTriggerBack) {
+            dispatchOnBackInvoked(mActiveCallback);
+        } else {
+            dispatchOnBackCancelled(mActiveCallback);
+        }
+    }
+
+    /**
+     * Called when the post commit animation is completed or timeout.
+     * This will trigger the real {@link IOnBackInvokedCallback} behavior.
+     */
+    @VisibleForTesting
+    void onBackAnimationFinished() {
+        if (!mPostCommitAnimationInProgress) {
+            return;
+        }
+        // Stop timeout runner.
+        mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
+        mPostCommitAnimationInProgress = false;
+
+        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()");
+
+        // Trigger the real back.
+        if (mBackNavigationInfo != null) {
+            IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
+            if (mTriggerBack) {
+                dispatchOnBackInvoked(callback);
+            } else {
+                dispatchOnBackCancelled(callback);
+            }
+        }
+
+        finishBackNavigation();
+    }
+
+    /**
+     * This should be called after the whole back navigation is completed.
+     */
     @VisibleForTesting
     void finishBackNavigation() {
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()");
@@ -571,10 +573,10 @@
         mTriggerBack = false;
         mShouldStartOnNextMoveEvent = false;
         mTouchTracker.reset();
+        mActiveCallback = null;
         if (backNavigationInfo == null) {
             return;
         }
-        stopTransition();
         if (mBackAnimationFinishedCallback != null) {
             try {
                 mBackAnimationFinishedCallback.onAnimationFinished(triggerBack);
@@ -586,19 +588,6 @@
         backNavigationInfo.onBackNavigationFinished(triggerBack);
     }
 
-    private void startTransition() {
-        if (mTransitionInProgress) {
-            return;
-        }
-        mTransitionInProgress = true;
-        mShellExecutor.executeDelayed(mResetTransitionRunnable, MAX_TRANSITION_DURATION);
-    }
-
-    void stopTransition() {
-        mShellExecutor.removeCallbacks(mResetTransitionRunnable);
-        mTransitionInProgress = false;
-    }
-
     private void createAdapter() {
         IBackAnimationRunner runner = new IBackAnimationRunner.Stub() {
             @Override
@@ -624,20 +613,18 @@
                     ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()");
                     runner.startAnimation(apps, wallpapers, nonApps,
                             BackAnimationController.this::onBackAnimationFinished);
+
                     if (apps.length >= 1) {
-                        final int backType = mBackNavigationInfo.getType();
-                        IOnBackInvokedCallback targetCallback = mAnimationDefinition.get(backType)
-                                .getCallback();
                         dispatchOnBackStarted(
-                                targetCallback, mTouchTracker.createStartEvent(apps[0]));
+                                mActiveCallback, mTouchTracker.createStartEvent(apps[0]));
                     }
 
                     if (!mBackGestureStarted) {
                         // if the down -> up gesture happened before animation start, we have to
                         // trigger the uninterruptible transition to finish the back animation.
-                        final BackEvent backFinish = mTouchTracker.createProgressEvent(1);
-                        startTransition();
-                        runner.consumeIfGestureFinished(backFinish);
+                        final BackEvent backFinish = mTouchTracker.createProgressEvent();
+                        dispatchOnBackProgressed(mActiveCallback, backFinish);
+                        startPostCommitAnimation();
                     }
                 });
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
index c53fcfc..d70b8f5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
@@ -18,12 +18,12 @@
 
 import static android.view.WindowManager.TRANSIT_OLD_UNSET;
 
+import android.annotation.NonNull;
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.IRemoteAnimationRunner;
 import android.view.RemoteAnimationTarget;
-import android.window.BackEvent;
 import android.window.IBackAnimationRunner;
 import android.window.IOnBackInvokedCallback;
 
@@ -38,11 +38,11 @@
     private final IOnBackInvokedCallback mCallback;
     private final IRemoteAnimationRunner mRunner;
 
-    private boolean mTriggerBack;
     // Whether we are waiting to receive onAnimationStart
     private boolean mWaitingAnimation;
 
-    BackAnimationRunner(IOnBackInvokedCallback callback, IRemoteAnimationRunner runner) {
+    BackAnimationRunner(@NonNull IOnBackInvokedCallback callback,
+            @NonNull IRemoteAnimationRunner runner) {
         mCallback = callback;
         mRunner = runner;
     }
@@ -83,25 +83,7 @@
         mWaitingAnimation = true;
     }
 
-    boolean onGestureFinished(boolean triggerBack) {
-        if (mWaitingAnimation) {
-            mTriggerBack = triggerBack;
-            return true;
-        }
-        return false;
-    }
-
-    void consumeIfGestureFinished(final BackEvent backFinish) {
-        Log.d(TAG, "Start transition due to gesture is finished");
-        try {
-            mCallback.onBackProgressed(backFinish);
-            if (mTriggerBack) {
-                mCallback.onBackInvoked();
-            } else {
-                mCallback.onBackCancelled();
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "dispatch error: ", e);
-        }
+    boolean isWaitingAnimation() {
+        return mWaitingAnimation;
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 7eccbf4..b603e03 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -93,7 +94,10 @@
     private IActivityTaskManager mActivityTaskManager;
 
     @Mock
-    private IOnBackInvokedCallback mIOnBackInvokedCallback;
+    private IOnBackInvokedCallback mAppCallback;
+
+    @Mock
+    private IOnBackInvokedCallback mAnimatorCallback;
 
     @Mock
     private IBackAnimationFinishedCallback mBackAnimationFinishedCallback;
@@ -105,8 +109,6 @@
     private ShellController mShellController;
 
     private BackAnimationController mController;
-
-    private int mEventTime = 0;
     private TestableContentResolver mContentResolver;
     private TestableLooper mTestableLooper;
 
@@ -126,16 +128,15 @@
                 mContentResolver);
         mController.setEnableUAnimation(true);
         mShellInit.init();
-        mEventTime = 0;
         mShellExecutor.flushAll();
     }
 
-    private void createNavigationInfo(int backType, IOnBackInvokedCallback onBackInvokedCallback) {
+    private void createNavigationInfo(int backType, boolean enableAnimation) {
         BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder()
                 .setType(backType)
                 .setOnBackNavigationDone(new RemoteCallback((bundle) -> {}))
-                .setOnBackInvokedCallback(onBackInvokedCallback)
-                .setPrepareRemoteAnimation(true);
+                .setOnBackInvokedCallback(mAppCallback)
+                .setPrepareRemoteAnimation(enableAnimation);
 
         createNavigationInfo(builder);
     }
@@ -176,26 +177,47 @@
     }
 
     @Test
-    public void verifyAnimationFinishes() {
-        RemoteAnimationTarget animationTarget = createAnimationTarget();
-        boolean[] backNavigationDone = new boolean[]{false};
-        boolean[] triggerBack = new boolean[]{false};
-        createNavigationInfo(new BackNavigationInfo.Builder()
-                .setType(BackNavigationInfo.TYPE_CROSS_ACTIVITY)
-                .setOnBackNavigationDone(
-                        new RemoteCallback(result -> {
-                            backNavigationDone[0] = true;
-                            triggerBack[0] = result.getBoolean(KEY_TRIGGER_BACK);
-                        })));
-        triggerBackGesture();
-        assertTrue("Navigation Done callback not called", backNavigationDone[0]);
-        assertTrue("TriggerBack should have been true", triggerBack[0]);
+    public void verifyNavigationFinishes() throws RemoteException {
+        final int[] testTypes = new int[] {BackNavigationInfo.TYPE_RETURN_TO_HOME,
+                BackNavigationInfo.TYPE_CROSS_TASK,
+                BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+                BackNavigationInfo.TYPE_DIALOG_CLOSE,
+                BackNavigationInfo.TYPE_CALLBACK };
+
+        for (int type: testTypes) {
+            registerAnimation(type);
+        }
+
+        for (int type: testTypes) {
+            boolean[] backNavigationDone = new boolean[]{false};
+            boolean[] triggerBack = new boolean[]{false};
+
+            createNavigationInfo(new BackNavigationInfo.Builder()
+                    .setType(type)
+                    .setOnBackInvokedCallback(mAppCallback)
+                    .setPrepareRemoteAnimation(true)
+                    .setOnBackNavigationDone(
+                            new RemoteCallback(result -> {
+                                backNavigationDone[0] = true;
+                                triggerBack[0] = result.getBoolean(KEY_TRIGGER_BACK);
+                            })));
+            triggerBackGesture();
+            simulateRemoteAnimationStart(type);
+            simulateRemoteAnimationFinished();
+            mShellExecutor.flushAll();
+
+            assertTrue("Navigation Done callback not called for "
+                    + BackNavigationInfo.typeToString(type), backNavigationDone[0]);
+            assertTrue("TriggerBack should have been true", triggerBack[0]);
+        }
     }
 
+
+
     @Test
     public void backToHome_dispatchesEvents() throws RemoteException {
-        mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
-        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, mIOnBackInvokedCallback);
+        registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
 
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
 
@@ -203,14 +225,16 @@
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
 
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+
+        verify(mAnimatorCallback).onBackStarted(any(BackEvent.class));
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
-        verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackEvent.class));
+        ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
+        verify(mAnimatorCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture());
 
         // Check that back invocation is dispatched.
         mController.setTriggerBack(true);   // Fake trigger back
         doMotionEvent(MotionEvent.ACTION_UP, 0);
-        verify(mIOnBackInvokedCallback).onBackInvoked();
+        verify(mAnimatorCallback).onBackInvoked();
     }
 
     @Test
@@ -223,97 +247,94 @@
                 mActivityTaskManager, mContext,
                 mContentResolver);
         shellInit.init();
-        mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
+        registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
 
-        IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class);
         ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
 
-        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback);
+        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, false);
 
         triggerBackGesture();
 
-        verify(appCallback, never()).onBackStarted(any(BackEvent.class));
-        verify(appCallback, never()).onBackProgressed(backEventCaptor.capture());
-        verify(appCallback, times(1)).onBackInvoked();
+        verify(mAppCallback, never()).onBackStarted(any());
+        verify(mAppCallback, never()).onBackProgressed(backEventCaptor.capture());
+        verify(mAppCallback, times(1)).onBackInvoked();
 
-        verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackEvent.class));
-        verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture());
-        verify(mIOnBackInvokedCallback, never()).onBackInvoked();
+        verify(mAnimatorCallback, never()).onBackStarted(any());
+        verify(mAnimatorCallback, never()).onBackProgressed(backEventCaptor.capture());
+        verify(mAnimatorCallback, never()).onBackInvoked();
         verify(mBackAnimationRunner, never()).onAnimationStart(
                 anyInt(), any(), any(), any(), any());
     }
 
     @Test
     public void ignoresGesture_transitionInProgress() throws RemoteException {
-        mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
-        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+        registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
 
         triggerBackGesture();
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
         // Check that back invocation is dispatched.
-        verify(mIOnBackInvokedCallback).onBackInvoked();
+        verify(mAnimatorCallback).onBackInvoked();
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
 
-        reset(mIOnBackInvokedCallback);
+        reset(mAnimatorCallback);
         reset(mBackAnimationRunner);
 
         // Verify that we prevent animation from restarting if another gestures happens before
         // the previous transition is finished.
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
-        verifyNoMoreInteractions(mIOnBackInvokedCallback);
-        mController.onBackAnimationFinished();
-        // Pretend the transition handler called finishAnimation.
-        mController.finishBackNavigation();
+        verifyNoMoreInteractions(mAnimatorCallback);
+
+        // Finish back navigation.
+        simulateRemoteAnimationFinished();
 
         // Verify that more events from a rejected swipe cannot start animation.
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
         doMotionEvent(MotionEvent.ACTION_UP, 0);
-        verifyNoMoreInteractions(mIOnBackInvokedCallback);
+        verifyNoMoreInteractions(mAnimatorCallback);
 
         // Verify that we start accepting gestures again once transition finishes.
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
 
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+        verify(mAnimatorCallback).onBackStarted(any());
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
     }
 
     @Test
     public void acceptsGesture_transitionTimeout() throws RemoteException {
-        mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
-        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+        registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
+
+        // In case it is still running in animation.
+        doNothing().when(mAnimatorCallback).onBackInvoked();
 
         triggerBackGesture();
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
 
-        reset(mIOnBackInvokedCallback);
-
         // Simulate transition timeout.
         mShellExecutor.flushAll();
-        mController.onBackAnimationFinished();
-        // Pretend the transition handler called finishAnimation.
-        mController.finishBackNavigation();
+        reset(mAnimatorCallback);
 
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+        verify(mAnimatorCallback).onBackStarted(any());
     }
 
-
     @Test
     public void cancelBackInvokeWhenLostFocus() throws RemoteException {
-        mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
+        registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
 
-        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
 
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         // Check that back start and progress is dispatched when first move.
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
 
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+        verify(mAnimatorCallback).onBackStarted(any());
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
 
         // Check that back invocation is dispatched.
@@ -323,11 +344,11 @@
         IBinder token = mock(IBinder.class);
         mController.mFocusObserver.focusLost(token);
         mShellExecutor.flushAll();
-        verify(mIOnBackInvokedCallback).onBackCancelled();
+        verify(mAnimatorCallback).onBackCancelled();
 
         // No more back invoke.
         doMotionEvent(MotionEvent.ACTION_UP, 0);
-        verify(mIOnBackInvokedCallback, never()).onBackInvoked();
+        verify(mAnimatorCallback, never()).onBackInvoked();
     }
 
     private void doMotionEvent(int actionDown, int coordinate) {
@@ -335,7 +356,6 @@
                 coordinate, coordinate,
                 actionDown,
                 BackEvent.EDGE_LEFT);
-        mEventTime += 10;
     }
 
     private void simulateRemoteAnimationStart(int type) throws RemoteException {
@@ -347,4 +367,14 @@
             mShellExecutor.flushAll();
         }
     }
+
+    private void simulateRemoteAnimationFinished() {
+        mController.onBackAnimationFinished();
+        mController.finishBackNavigation();
+    }
+
+    private void registerAnimation(int type) {
+        mController.registerAnimation(type,
+                new BackAnimationRunner(mAnimatorCallback, mBackAnimationRunner));
+    }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
index 151b50cd..60599d4 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
@@ -30,14 +30,14 @@
     val name: String
 
     /** The display name of this page provider, for better readability. */
-    val displayName: String?
-        get() = null
+    val displayName: String
+        get() = name
 
     /** The page parameters, default is no parameters. */
     val parameter: List<NamedNavArgument>
         get() = emptyList()
 
-    fun getTitle(arguments: Bundle?): String = displayName ?: name
+    fun getTitle(arguments: Bundle?): String = displayName
 
     fun buildEntry(arguments: Bundle?): List<SettingsEntry> = emptyList()
 
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java b/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
index 798217f..04bd135 100644
--- a/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
@@ -49,6 +49,15 @@
     //flag to decide if intent needs to be resolved cross profile if pkgName is already defined
     public static final int FLAG_IS_PACKAGE_FOR_FILTER = 0x00000008;
 
+    /*
+    This flag, denotes if further cross profile resolution is allowed, e.g. if profile#0 is linked
+    to profile#1 and profile#2 . When intent resolution from profile#1 is started we resolve it in
+    profile#1 and profile#0. The profile#0 is also linked to profile#2, we will only resolve in
+    profile#2 if CrossProfileIntentFilter between profile#1 and profile#0 have set flag
+    FLAG_ALLOW_CHAINED_RESOLUTION.
+     */
+    public static final int FLAG_ALLOW_CHAINED_RESOLUTION = 0x00000010;
+
     private static final String TAG = "CrossProfileIntentFilter";
 
     /**
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
index 5ae4cab..4362956 100644
--- a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
@@ -36,14 +36,19 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 
 import com.android.server.LocalServices;
 import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
 import com.android.server.pm.verify.domain.DomainVerificationUtils;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Queue;
+import java.util.Set;
 import java.util.function.Function;
 
 /**
@@ -115,73 +120,111 @@
             Intent intent, String resolvedType, int userId, long flags, String pkgName,
             boolean hasNonNegativePriorityResult,
             Function<String, PackageStateInternal> pkgSettingFunction) {
-
+        Queue<Integer> pendingUsers = new ArrayDeque<>();
+        Set<Integer> visitedUserIds = new HashSet<>();
+        SparseBooleanArray hasNonNegativePriorityResultFromParent = new SparseBooleanArray();
+        visitedUserIds.add(userId);
+        pendingUsers.add(userId);
+        hasNonNegativePriorityResultFromParent.put(userId, hasNonNegativePriorityResult);
+        UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
         List<CrossProfileDomainInfo> crossProfileDomainInfos = new ArrayList<>();
+        while (!pendingUsers.isEmpty()) {
+            int currentUserId = pendingUsers.poll();
+            List<CrossProfileIntentFilter> matchingFilters =
+                    computer.getMatchingCrossProfileIntentFilters(intent, resolvedType,
+                            currentUserId);
 
-        List<CrossProfileIntentFilter> matchingFilters =
-                computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, userId);
-
-        if (matchingFilters == null || matchingFilters.isEmpty()) {
-            /** if intent is web intent, checking if parent profile should handle the intent even
-            if there is no matching filter. The configuration is based on user profile
-            restriction android.os.UserManager#ALLOW_PARENT_PROFILE_APP_LINKING **/
-            if (intent.hasWebURI()) {
-                UserInfo parent = computer.getProfileParent(userId);
-                if (parent != null) {
-                    CrossProfileDomainInfo generalizedCrossProfileDomainInfo = computer
-                            .getCrossProfileDomainPreferredLpr(intent, resolvedType, flags, userId,
-                                    parent.id);
-                    if (generalizedCrossProfileDomainInfo != null) {
-                        crossProfileDomainInfos.add(generalizedCrossProfileDomainInfo);
+            if (matchingFilters == null || matchingFilters.isEmpty()) {
+                /** if intent is web intent, checking if parent profile should handle the intent
+                 * even if there is no matching filter. The configuration is based on user profile
+                 * restriction android.os.UserManager#ALLOW_PARENT_PROFILE_APP_LINKING **/
+                if (currentUserId == userId && intent.hasWebURI()) {
+                    UserInfo parent = computer.getProfileParent(currentUserId);
+                    if (parent != null) {
+                        CrossProfileDomainInfo generalizedCrossProfileDomainInfo = computer
+                                .getCrossProfileDomainPreferredLpr(intent, resolvedType, flags,
+                                        currentUserId, parent.id);
+                        if (generalizedCrossProfileDomainInfo != null) {
+                            crossProfileDomainInfos.add(generalizedCrossProfileDomainInfo);
+                        }
                     }
                 }
+                continue;
             }
-            return crossProfileDomainInfos;
-        }
 
-        UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
-        UserInfo sourceUserInfo = umInternal.getUserInfo(userId);
+            UserInfo sourceUserInfo = umInternal.getUserInfo(currentUserId);
 
-       // Grouping the CrossProfileIntentFilters based on targerId
-        SparseArray<List<CrossProfileIntentFilter>> crossProfileIntentFiltersByUser =
-                new SparseArray<>();
+            // Grouping the CrossProfileIntentFilters based on targerId
+            SparseArray<List<CrossProfileIntentFilter>> crossProfileIntentFiltersByUser =
+                    new SparseArray<>();
 
-        for (int index = 0; index < matchingFilters.size(); index++) {
-            CrossProfileIntentFilter crossProfileIntentFilter = matchingFilters.get(index);
+            for (int index = 0; index < matchingFilters.size(); index++) {
+                CrossProfileIntentFilter crossProfileIntentFilter = matchingFilters.get(index);
 
-            if (!crossProfileIntentFiltersByUser
-                    .contains(crossProfileIntentFilter.mTargetUserId)) {
-                crossProfileIntentFiltersByUser.put(crossProfileIntentFilter.mTargetUserId,
-                        new ArrayList<>());
+                if (!crossProfileIntentFiltersByUser
+                        .contains(crossProfileIntentFilter.mTargetUserId)) {
+                    crossProfileIntentFiltersByUser.put(crossProfileIntentFilter.mTargetUserId,
+                            new ArrayList<>());
+                }
+                crossProfileIntentFiltersByUser.get(crossProfileIntentFilter.mTargetUserId)
+                        .add(crossProfileIntentFilter);
             }
-            crossProfileIntentFiltersByUser.get(crossProfileIntentFilter.mTargetUserId)
-                    .add(crossProfileIntentFilter);
-        }
 
-        /*
-         For each target user, we would call their corresponding strategy
-         {@link CrossProfileResolver} to resolve intent in corresponding user
-         */
-        for (int index = 0; index < crossProfileIntentFiltersByUser.size(); index++) {
+            /*
+             For each target user, we would call their corresponding strategy
+             {@link CrossProfileResolver} to resolve intent in corresponding user
+             */
+            for (int index = 0; index < crossProfileIntentFiltersByUser.size(); index++) {
 
-            UserInfo targetUserInfo = umInternal.getUserInfo(crossProfileIntentFiltersByUser
-                    .keyAt(index));
+                int targetUserId = crossProfileIntentFiltersByUser.keyAt(index);
 
-            // Choosing strategy based on source and target user
-            CrossProfileResolver crossProfileResolver =
-                    chooseCrossProfileResolver(computer, sourceUserInfo, targetUserInfo);
+                //if user is already visited then skip resolution for particular user.
+                if (visitedUserIds.contains(targetUserId)) {
+                    continue;
+                }
+
+                UserInfo targetUserInfo = umInternal.getUserInfo(targetUserId);
+
+                // Choosing strategy based on source and target user
+                CrossProfileResolver crossProfileResolver =
+                        chooseCrossProfileResolver(computer, sourceUserInfo, targetUserInfo);
 
             /*
             If {@link CrossProfileResolver} is available for source,target pair we will call it to
             get {@link CrossProfileDomainInfo}s from that user.
              */
-            if (crossProfileResolver != null) {
-                List<CrossProfileDomainInfo> crossProfileInfos = crossProfileResolver
-                        .resolveIntent(computer, intent, resolvedType, userId,
-                                crossProfileIntentFiltersByUser.keyAt(index), flags, pkgName,
-                                crossProfileIntentFiltersByUser.valueAt(index),
-                                hasNonNegativePriorityResult, pkgSettingFunction);
-                crossProfileDomainInfos.addAll(crossProfileInfos);
+                if (crossProfileResolver != null) {
+                    List<CrossProfileDomainInfo> crossProfileInfos = crossProfileResolver
+                            .resolveIntent(computer, intent, resolvedType, currentUserId,
+                                    targetUserId, flags, pkgName,
+                                    crossProfileIntentFiltersByUser.valueAt(index),
+                                    hasNonNegativePriorityResultFromParent.get(currentUserId),
+                                    pkgSettingFunction);
+                    crossProfileDomainInfos.addAll(crossProfileInfos);
+
+                    hasNonNegativePriorityResultFromParent.put(targetUserId,
+                            hasNonNegativePriority(crossProfileInfos));
+
+                    /*
+                    Adding target user to queue if flag
+                    {@link CrossProfileIntentFilter#FLAG_ALLOW_CHAINED_RESOLUTION} is set for any
+                    {@link CrossProfileIntentFilter}
+                     */
+                    boolean allowChainedResolution = false;
+                    for (int filterIndex = 0; filterIndex < crossProfileIntentFiltersByUser
+                            .valueAt(index).size(); filterIndex++) {
+                        if ((CrossProfileIntentFilter
+                                .FLAG_ALLOW_CHAINED_RESOLUTION & crossProfileIntentFiltersByUser
+                                .valueAt(index).get(filterIndex).mFlags) != 0) {
+                            allowChainedResolution = true;
+                            break;
+                        }
+                    }
+                    if (allowChainedResolution) {
+                        pendingUsers.add(targetUserId);
+                    }
+                    visitedUserIds.add(targetUserId);
+                }
             }
         }
 
@@ -237,7 +280,7 @@
 
     /**
      * Returns true if we source user can reach target user for given intent. The source can
-     * directly or indirectly reach to target. This will perform depth first search to check if
+     * directly or indirectly reach to target. This will perform breadth first search to check if
      * source can reach target.
      * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
      * @param intent request
@@ -251,13 +294,38 @@
             @UserIdInt int targetUserId) {
         if (sourceUserId == targetUserId) return true;
 
-        List<CrossProfileIntentFilter> matches =
-                computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, sourceUserId);
-        if (matches != null) {
-            for (int index = 0; index < matches.size(); index++) {
-                CrossProfileIntentFilter crossProfileIntentFilter = matches.get(index);
-                if (crossProfileIntentFilter.mTargetUserId == targetUserId) {
-                    return true;
+        Queue<Integer> pendingUsers = new ArrayDeque<>();
+        Set<Integer> visitedUserIds = new HashSet<>();
+        visitedUserIds.add(sourceUserId);
+        pendingUsers.add(sourceUserId);
+
+        while (!pendingUsers.isEmpty()) {
+            int currentUserId = pendingUsers.poll();
+
+            List<CrossProfileIntentFilter> matches =
+                    computer.getMatchingCrossProfileIntentFilters(intent, resolvedType,
+                            currentUserId);
+            if (matches != null) {
+                for (int index = 0; index < matches.size(); index++) {
+                    CrossProfileIntentFilter crossProfileIntentFilter = matches.get(index);
+                    if (crossProfileIntentFilter.mTargetUserId == targetUserId) {
+                        return true;
+                    }
+                    if (visitedUserIds.contains(crossProfileIntentFilter.mTargetUserId)) {
+                        continue;
+                    }
+
+                    /*
+                     If source cannot directly reach to target, we will add
+                     CrossProfileIntentFilter.mTargetUserId user to queue to check if target user
+                     can be reached via CrossProfileIntentFilter.mTargetUserId i.e. it can be
+                     indirectly reached through chained/linked profiles.
+                     */
+                    if ((CrossProfileIntentFilter.FLAG_ALLOW_CHAINED_RESOLUTION
+                            & crossProfileIntentFilter.mFlags) != 0) {
+                        pendingUsers.add(crossProfileIntentFilter.mTargetUserId);
+                        visitedUserIds.add(crossProfileIntentFilter.mTargetUserId);
+                    }
                 }
             }
         }
@@ -605,4 +673,14 @@
 
         return resolveInfoList;
     }
+
+    /**
+     * @param crossProfileDomainInfos list of cross profile domain info in descending priority order
+     * @return if the list contains a resolve info with non-negative priority
+     */
+    private boolean hasNonNegativePriority(List<CrossProfileDomainInfo> crossProfileDomainInfos) {
+        return crossProfileDomainInfos.size() > 0
+                && crossProfileDomainInfos.get(0).mResolveInfo != null
+                && crossProfileDomainInfos.get(0).mResolveInfo.priority >= 0;
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
index 9c8e72c..f5029ec 100644
--- a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
@@ -71,7 +71,7 @@
     private static final String TEST_APP_PACKAGE = "com.android.servicestests.apps.jobtestapp";
     private static final String TEST_APP_ACTIVITY = TEST_APP_PACKAGE + ".TestJobActivity";
     private static final long POLL_INTERVAL = 500;
-    private static final long DEFAULT_WAIT_TIMEOUT = 5000;
+    private static final long DEFAULT_WAIT_TIMEOUT = 10_000;
 
     private Context mContext;
     private AppOpsManager mAppOpsManager;
diff --git a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
index 3e79407..b8585f2 100644
--- a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
+++ b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
@@ -34,7 +34,8 @@
     public boolean onStartJob(JobParameters params) {
         Log.i(TAG, "Test job executing: " + params.getJobId());
         Intent reportJobStartIntent = new Intent(ACTION_JOB_STARTED);
-        reportJobStartIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params);
+        reportJobStartIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params)
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
         sendBroadcast(reportJobStartIntent);
         return true;
     }
@@ -43,7 +44,8 @@
     public boolean onStopJob(JobParameters params) {
         Log.i(TAG, "Test job stopped executing: " + params.getJobId());
         Intent reportJobStopIntent = new Intent(ACTION_JOB_STOPPED);
-        reportJobStopIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params);
+        reportJobStopIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params)
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
         sendBroadcast(reportJobStopIntent);
         // Deadline constraint is dropped on reschedule, so it's more reliable to use a new job.
         return false;