Merge "Update android.app.ui_rich_ongoing to use the new FR" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index a131ea7..a071668 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -33929,9 +33929,12 @@
   public class RemoteCallbackList<E extends android.os.IInterface> {
     ctor public RemoteCallbackList();
     method public int beginBroadcast();
+    method @FlaggedApi("android.os.binder_frozen_state_change_callback") public void broadcast(@NonNull java.util.function.Consumer<E>);
     method public void finishBroadcast();
     method public Object getBroadcastCookie(int);
     method public E getBroadcastItem(int);
+    method @FlaggedApi("android.os.binder_frozen_state_change_callback") public int getFrozenCalleePolicy();
+    method @FlaggedApi("android.os.binder_frozen_state_change_callback") public int getMaxQueueSize();
     method public Object getRegisteredCallbackCookie(int);
     method public int getRegisteredCallbackCount();
     method public E getRegisteredCallbackItem(int);
@@ -33941,6 +33944,16 @@
     method public boolean register(E);
     method public boolean register(E, Object);
     method public boolean unregister(E);
+    field @FlaggedApi("android.os.binder_frozen_state_change_callback") public static final int FROZEN_CALLEE_POLICY_DROP = 3; // 0x3
+    field @FlaggedApi("android.os.binder_frozen_state_change_callback") public static final int FROZEN_CALLEE_POLICY_ENQUEUE_ALL = 1; // 0x1
+    field @FlaggedApi("android.os.binder_frozen_state_change_callback") public static final int FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT = 2; // 0x2
+    field @FlaggedApi("android.os.binder_frozen_state_change_callback") public static final int FROZEN_CALLEE_POLICY_UNSET = 0; // 0x0
+  }
+
+  @FlaggedApi("android.os.binder_frozen_state_change_callback") public static final class RemoteCallbackList.Builder<E extends android.os.IInterface> {
+    ctor public RemoteCallbackList.Builder(int);
+    method @NonNull public android.os.RemoteCallbackList<E> build();
+    method @NonNull public android.os.RemoteCallbackList.Builder setMaxQueueSize(int);
   }
 
   public class RemoteException extends android.util.AndroidException {
diff --git a/core/java/android/os/RemoteCallbackList.java b/core/java/android/os/RemoteCallbackList.java
index 769cbdd..f82c822 100644
--- a/core/java/android/os/RemoteCallbackList.java
+++ b/core/java/android/os/RemoteCallbackList.java
@@ -16,11 +16,18 @@
 
 package android.os;
 
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.util.ArrayMap;
 import android.util.Slog;
 
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 
@@ -30,7 +37,7 @@
  * {@link android.app.Service} to its clients.  In particular, this:
  *
  * <ul>
- * <li> Keeps track of a set of registered {@link IInterface} callbacks,
+ * <li> Keeps track of a set of registered {@link IInterface} objects,
  * taking care to identify them through their underlying unique {@link IBinder}
  * (by calling {@link IInterface#asBinder IInterface.asBinder()}.
  * <li> Attaches a {@link IBinder.DeathRecipient IBinder.DeathRecipient} to
@@ -47,7 +54,7 @@
  * the registered clients, use {@link #beginBroadcast},
  * {@link #getBroadcastItem}, and {@link #finishBroadcast}.
  *
- * <p>If a registered callback's process goes away, this class will take
+ * <p>If a registered interface's process goes away, this class will take
  * care of automatically removing it from the list.  If you want to do
  * additional work in this situation, you can create a subclass that
  * implements the {@link #onCallbackDied} method.
@@ -56,78 +63,310 @@
 public class RemoteCallbackList<E extends IInterface> {
     private static final String TAG = "RemoteCallbackList";
 
+    private static final int DEFAULT_MAX_QUEUE_SIZE = 1000;
+
+
+    /**
+     * @hide
+     */
+    @IntDef(prefix = {"FROZEN_CALLEE_POLICY_"}, value = {
+            FROZEN_CALLEE_POLICY_UNSET,
+            FROZEN_CALLEE_POLICY_ENQUEUE_ALL,
+            FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT,
+            FROZEN_CALLEE_POLICY_DROP,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface FrozenCalleePolicy {
+    }
+
+    /**
+     * Callbacks are invoked immediately regardless of the frozen state of the target process.
+     *
+     * Not recommended. Only exists for backward-compatibility. This represents the behavior up to
+     * SDK 35. Starting with SDK 36, clients should set a policy to govern callback invocations when
+     * recipients are frozen.
+     */
+    @FlaggedApi(Flags.FLAG_BINDER_FROZEN_STATE_CHANGE_CALLBACK)
+    public static final int FROZEN_CALLEE_POLICY_UNSET = 0;
+
+    /**
+     * When the callback recipient's process is frozen, callbacks are enqueued so they're invoked
+     * after the recipient is unfrozen.
+     *
+     * This is commonly used when the recipient wants to receive all callbacks without losing any
+     * history, e.g. the recipient maintains a running count of events that occurred.
+     *
+     * Queued callbacks are invoked in the order they were originally broadcasted.
+     */
+    @FlaggedApi(Flags.FLAG_BINDER_FROZEN_STATE_CHANGE_CALLBACK)
+    public static final int FROZEN_CALLEE_POLICY_ENQUEUE_ALL = 1;
+
+    /**
+     * When the callback recipient's process is frozen, only the most recent callback is enqueued,
+     * which is later invoked after the recipient is unfrozen.
+     *
+     * This can be used when only the most recent state matters, for instance when clients are
+     * listening to screen brightness changes.
+     */
+    @FlaggedApi(Flags.FLAG_BINDER_FROZEN_STATE_CHANGE_CALLBACK)
+    public static final int FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT = 2;
+
+    /**
+     * When the callback recipient's process is frozen, callbacks are suppressed as if they never
+     * happened.
+     *
+     * This could be useful in the case where the recipient wishes to react to callbacks only when
+     * they occur while the recipient is not frozen. For example, certain network events are only
+     * worth responding to if the response can be immediate. Another example is recipients having
+     * another way of getting the latest state once it's unfrozen. Therefore there is no need to
+     * save callbacks that happened while the recipient was frozen.
+     */
+    @FlaggedApi(Flags.FLAG_BINDER_FROZEN_STATE_CHANGE_CALLBACK)
+    public static final int FROZEN_CALLEE_POLICY_DROP = 3;
+
     @UnsupportedAppUsage
-    /*package*/ ArrayMap<IBinder, Callback> mCallbacks
-            = new ArrayMap<IBinder, Callback>();
+    /*package*/ ArrayMap<IBinder, Interface> mInterfaces = new ArrayMap<IBinder, Interface>();
     private Object[] mActiveBroadcast;
     private int mBroadcastCount = -1;
     private boolean mKilled = false;
     private StringBuilder mRecentCallers;
 
-    private final class Callback implements IBinder.DeathRecipient {
-        final E mCallback;
-        final Object mCookie;
+    private final @FrozenCalleePolicy int mFrozenCalleePolicy;
+    private final int mMaxQueueSize;
 
-        Callback(E callback, Object cookie) {
-            mCallback = callback;
+    private final class Interface implements IBinder.DeathRecipient,
+            IBinder.FrozenStateChangeCallback {
+        final IBinder mBinder;
+        final E mInterface;
+        final Object mCookie;
+        final Queue<Consumer<E>> mCallbackQueue;
+        int mCurrentState = IBinder.FrozenStateChangeCallback.STATE_UNFROZEN;
+
+        Interface(E callbackInterface, Object cookie) {
+            mBinder = callbackInterface.asBinder();
+            mInterface = callbackInterface;
             mCookie = cookie;
+            mCallbackQueue = mFrozenCalleePolicy == FROZEN_CALLEE_POLICY_ENQUEUE_ALL
+                || mFrozenCalleePolicy == FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT
+                ? new ConcurrentLinkedQueue<>() : null;
+        }
+
+        @Override
+        public synchronized void onFrozenStateChanged(@NonNull IBinder who, int state) {
+            if (state == STATE_UNFROZEN && mCallbackQueue != null) {
+                while (!mCallbackQueue.isEmpty()) {
+                    Consumer<E> callback = mCallbackQueue.poll();
+                    callback.accept(mInterface);
+                }
+            }
+            mCurrentState = state;
+        }
+
+        void addCallback(@NonNull Consumer<E> callback) {
+            if (mFrozenCalleePolicy == FROZEN_CALLEE_POLICY_UNSET) {
+                callback.accept(mInterface);
+                return;
+            }
+            synchronized (this) {
+                if (mCurrentState == STATE_UNFROZEN) {
+                    callback.accept(mInterface);
+                    return;
+                }
+                switch (mFrozenCalleePolicy) {
+                    case FROZEN_CALLEE_POLICY_ENQUEUE_ALL:
+                        if (mCallbackQueue.size() >= mMaxQueueSize) {
+                            mCallbackQueue.poll();
+                        }
+                        mCallbackQueue.offer(callback);
+                        break;
+                    case FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT:
+                        mCallbackQueue.clear();
+                        mCallbackQueue.offer(callback);
+                        break;
+                    case FROZEN_CALLEE_POLICY_DROP:
+                        // Do nothing. Just ignore the callback.
+                        break;
+                    case FROZEN_CALLEE_POLICY_UNSET:
+                        // Do nothing. Should have returned at the start of the method.
+                        break;
+                }
+            }
+        }
+
+        public void maybeSubscribeToFrozenCallback() throws RemoteException {
+            if (mFrozenCalleePolicy != FROZEN_CALLEE_POLICY_UNSET) {
+                mBinder.addFrozenStateChangeCallback(this);
+            }
+        }
+
+        public void maybeUnsubscribeFromFrozenCallback() {
+            if (mFrozenCalleePolicy != FROZEN_CALLEE_POLICY_UNSET) {
+                mBinder.removeFrozenStateChangeCallback(this);
+            }
         }
 
         public void binderDied() {
-            synchronized (mCallbacks) {
-                mCallbacks.remove(mCallback.asBinder());
+            synchronized (mInterfaces) {
+                mInterfaces.remove(mBinder);
+                maybeUnsubscribeFromFrozenCallback();
             }
-            onCallbackDied(mCallback, mCookie);
+            onCallbackDied(mInterface, mCookie);
         }
     }
 
     /**
+     * Builder for {@link RemoteCallbackList}.
+     *
+     * @param <E> The remote callback interface type.
+     */
+    @FlaggedApi(Flags.FLAG_BINDER_FROZEN_STATE_CHANGE_CALLBACK)
+    public static final class Builder<E extends IInterface> {
+        private @FrozenCalleePolicy int mFrozenCalleePolicy;
+        private int mMaxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
+
+        /**
+         * Creates a Builder for {@link RemoteCallbackList}.
+         *
+         * @param frozenCalleePolicy When the callback recipient's process is frozen, this parameter
+         * specifies when/whether callbacks are invoked. It's important to choose a strategy that's
+         * right for the use case. Leaving the policy unset with {@link #FROZEN_CALLEE_POLICY_UNSET}
+         * is not recommended as it allows callbacks to be invoked while the recipient is frozen.
+         */
+        public Builder(@FrozenCalleePolicy int frozenCalleePolicy) {
+            mFrozenCalleePolicy = frozenCalleePolicy;
+        }
+
+        /**
+         * Sets the max queue size.
+         *
+         * @param maxQueueSize The max size limit on the queue that stores callbacks added when the
+         * recipient's process is frozen. Once the limit is reached, the oldest callback is dropped
+         * to keep the size under the limit. Should only be called for
+         * {@link #FROZEN_CALLEE_POLICY_ENQUEUE_ALL}.
+         *
+         * @return This builder.
+         * @throws IllegalArgumentException if the maxQueueSize is not positive.
+         * @throws UnsupportedOperationException if frozenCalleePolicy is not
+         * {@link #FROZEN_CALLEE_POLICY_ENQUEUE_ALL}.
+         */
+        public @NonNull Builder setMaxQueueSize(int maxQueueSize) {
+            if (maxQueueSize <= 0) {
+                throw new IllegalArgumentException("maxQueueSize must be positive");
+            }
+            if (mFrozenCalleePolicy != FROZEN_CALLEE_POLICY_ENQUEUE_ALL) {
+                throw new UnsupportedOperationException(
+                        "setMaxQueueSize can only be called for FROZEN_CALLEE_POLICY_ENQUEUE_ALL");
+            }
+            mMaxQueueSize = maxQueueSize;
+            return this;
+        }
+
+        /**
+         * Builds and returns a {@link RemoteCallbackList}.
+         *
+         * @return The built {@link RemoteCallbackList} object.
+         */
+        public @NonNull RemoteCallbackList<E> build() {
+            return new RemoteCallbackList<E>(mFrozenCalleePolicy, mMaxQueueSize);
+        }
+    }
+
+    /**
+     * Returns the frozen callee policy.
+     *
+     * @return The frozen callee policy.
+     */
+    @FlaggedApi(Flags.FLAG_BINDER_FROZEN_STATE_CHANGE_CALLBACK)
+    public @FrozenCalleePolicy int getFrozenCalleePolicy() {
+        return mFrozenCalleePolicy;
+    }
+
+    /**
+     * Returns the max queue size.
+     *
+     * @return The max queue size.
+     */
+    @FlaggedApi(Flags.FLAG_BINDER_FROZEN_STATE_CHANGE_CALLBACK)
+    public int getMaxQueueSize() {
+        return mMaxQueueSize;
+    }
+
+    /**
+     * Creates a RemoteCallbackList with {@link #FROZEN_CALLEE_POLICY_UNSET}. This is equivalent to
+     * <pre>
+     * new RemoteCallbackList.Build(RemoteCallbackList.FROZEN_CALLEE_POLICY_UNSET).build()
+     * </pre>
+     */
+    public RemoteCallbackList() {
+        this(FROZEN_CALLEE_POLICY_UNSET, DEFAULT_MAX_QUEUE_SIZE);
+    }
+
+    /**
+     * Creates a RemoteCallbackList with the specified frozen callee policy.
+     *
+     * @param frozenCalleePolicy When the callback recipient's process is frozen, this parameter
+     * specifies when/whether callbacks are invoked. It's important to choose a strategy that's
+     * right for the use case. Leaving the policy unset with {@link #FROZEN_CALLEE_POLICY_UNSET}
+     * is not recommended as it allows callbacks to be invoked while the recipient is frozen.
+     *
+     * @param maxQueueSize The max size limit on the queue that stores callbacks added when the
+     * recipient's process is frozen. Once the limit is reached, the oldest callbacks would be
+     * dropped to keep the size under limit. Ignored except for
+     * {@link #FROZEN_CALLEE_POLICY_ENQUEUE_ALL}.
+     */
+    private RemoteCallbackList(@FrozenCalleePolicy int frozenCalleePolicy, int maxQueueSize) {
+        mFrozenCalleePolicy = frozenCalleePolicy;
+        mMaxQueueSize = maxQueueSize;
+    }
+
+    /**
      * Simple version of {@link RemoteCallbackList#register(E, Object)}
      * that does not take a cookie object.
      */
-    public boolean register(E callback) {
-        return register(callback, null);
+    public boolean register(E callbackInterface) {
+        return register(callbackInterface, null);
     }
 
     /**
-     * Add a new callback to the list.  This callback will remain in the list
+     * Add a new interface to the list.  This interface will remain in the list
      * until a corresponding call to {@link #unregister} or its hosting process
-     * goes away. If the callback was already registered (determined by
-     * checking to see if the {@link IInterface#asBinder callback.asBinder()}
-     * object is already in the list), then it will be replaced with the new callback.
+     * goes away.  If the interface was already registered (determined by
+     * checking to see if the {@link IInterface#asBinder callbackInterface.asBinder()}
+     * object is already in the list), then it will be replaced with the new interface.
      * Registrations are not counted; a single call to {@link #unregister}
-     * will remove a callback after any number calls to register it.
+     * will remove an interface after any number calls to register it.
      *
-     * @param callback The callback interface to be added to the list.  Must
+     * @param callbackInterface The callback interface to be added to the list.  Must
      * not be null -- passing null here will cause a NullPointerException.
      * Most services will want to check for null before calling this with
      * an object given from a client, so that clients can't crash the
      * service with bad data.
      *
      * @param cookie Optional additional data to be associated with this
-     * callback.
+     * interface.
      *
-     * @return Returns true if the callback was successfully added to the list.
+     * @return Returns true if the interface was successfully added to the list.
      * Returns false if it was not added, either because {@link #kill} had
-     * previously been called or the callback's process has gone away.
+     * previously been called or the interface's process has gone away.
      *
      * @see #unregister
      * @see #kill
      * @see #onCallbackDied
      */
-    public boolean register(E callback, Object cookie) {
-        synchronized (mCallbacks) {
+    public boolean register(E callbackInterface, Object cookie) {
+        synchronized (mInterfaces) {
             if (mKilled) {
                 return false;
             }
             // Flag unusual case that could be caused by a leak. b/36778087
-            logExcessiveCallbacks();
-            IBinder binder = callback.asBinder();
+            logExcessiveInterfaces();
+            IBinder binder = callbackInterface.asBinder();
             try {
-                Callback cb = new Callback(callback, cookie);
-                unregister(callback);
-                binder.linkToDeath(cb, 0);
-                mCallbacks.put(binder, cb);
+                Interface i = new Interface(callbackInterface, cookie);
+                unregister(callbackInterface);
+                binder.linkToDeath(i, 0);
+                i.maybeSubscribeToFrozenCallback();
+                mInterfaces.put(binder, i);
                 return true;
             } catch (RemoteException e) {
                 return false;
@@ -136,27 +375,28 @@
     }
 
     /**
-     * Remove from the list a callback that was previously added with
+     * Remove from the list an interface that was previously added with
      * {@link #register}.  This uses the
-     * {@link IInterface#asBinder callback.asBinder()} object to correctly
+     * {@link IInterface#asBinder callbackInterface.asBinder()} object to correctly
      * find the previous registration.
      * Registrations are not counted; a single unregister call will remove
-     * a callback after any number calls to {@link #register} for it.
+     * an interface after any number calls to {@link #register} for it.
      *
-     * @param callback The callback to be removed from the list.  Passing
+     * @param callbackInterface The interface to be removed from the list.  Passing
      * null here will cause a NullPointerException, so you will generally want
      * to check for null before calling.
      *
-     * @return Returns true if the callback was found and unregistered.  Returns
-     * false if the given callback was not found on the list.
+     * @return Returns true if the interface was found and unregistered.  Returns
+     * false if the given interface was not found on the list.
      *
      * @see #register
      */
-    public boolean unregister(E callback) {
-        synchronized (mCallbacks) {
-            Callback cb = mCallbacks.remove(callback.asBinder());
-            if (cb != null) {
-                cb.mCallback.asBinder().unlinkToDeath(cb, 0);
+    public boolean unregister(E callbackInterface) {
+        synchronized (mInterfaces) {
+            Interface i = mInterfaces.remove(callbackInterface.asBinder());
+            if (i != null) {
+                i.mInterface.asBinder().unlinkToDeath(i, 0);
+                i.maybeUnsubscribeFromFrozenCallback();
                 return true;
             }
             return false;
@@ -164,20 +404,21 @@
     }
 
     /**
-     * Disable this callback list.  All registered callbacks are unregistered,
+     * Disable this interface list.  All registered interfaces are unregistered,
      * and the list is disabled so that future calls to {@link #register} will
      * fail.  This should be used when a Service is stopping, to prevent clients
-     * from registering callbacks after it is stopped.
+     * from registering interfaces after it is stopped.
      *
      * @see #register
      */
     public void kill() {
-        synchronized (mCallbacks) {
-            for (int cbi=mCallbacks.size()-1; cbi>=0; cbi--) {
-                Callback cb = mCallbacks.valueAt(cbi);
-                cb.mCallback.asBinder().unlinkToDeath(cb, 0);
+        synchronized (mInterfaces) {
+            for (int cbi = mInterfaces.size() - 1; cbi >= 0; cbi--) {
+                Interface i = mInterfaces.valueAt(cbi);
+                i.mInterface.asBinder().unlinkToDeath(i, 0);
+                i.maybeUnsubscribeFromFrozenCallback();
             }
-            mCallbacks.clear();
+            mInterfaces.clear();
             mKilled = true;
         }
     }
@@ -186,15 +427,15 @@
      * Old version of {@link #onCallbackDied(E, Object)} that
      * does not provide a cookie.
      */
-    public void onCallbackDied(E callback) {
+    public void onCallbackDied(E callbackInterface) {
     }
     
     /**
-     * Called when the process hosting a callback in the list has gone away.
+     * Called when the process hosting an interface in the list has gone away.
      * The default implementation calls {@link #onCallbackDied(E)}
      * for backwards compatibility.
      * 
-     * @param callback The callback whose process has died.  Note that, since
+     * @param callbackInterface The interface whose process has died.  Note that, since
      * its process has died, you can not make any calls on to this interface.
      * You can, however, retrieve its IBinder and compare it with another
      * IBinder to see if it is the same object.
@@ -203,13 +444,15 @@
      * 
      * @see #register
      */
-    public void onCallbackDied(E callback, Object cookie) {
-        onCallbackDied(callback);
+    public void onCallbackDied(E callbackInterface, Object cookie) {
+        onCallbackDied(callbackInterface);
     }
 
     /**
-     * Prepare to start making calls to the currently registered callbacks.
-     * This creates a copy of the callback list, which you can retrieve items
+     * Use {@link #broadcast(Consumer)} instead to ensure proper handling of frozen processes.
+     *
+     * Prepare to start making calls to the currently registered interfaces.
+     * This creates a copy of the interface list, which you can retrieve items
      * from using {@link #getBroadcastItem}.  Note that only one broadcast can
      * be active at a time, so you must be sure to always call this from the
      * same thread (usually by scheduling with {@link Handler}) or
@@ -219,44 +462,56 @@
      * <p>A typical loop delivering a broadcast looks like this:
      *
      * <pre>
-     * int i = callbacks.beginBroadcast();
+     * int i = interfaces.beginBroadcast();
      * while (i &gt; 0) {
      *     i--;
      *     try {
-     *         callbacks.getBroadcastItem(i).somethingHappened();
+     *         interfaces.getBroadcastItem(i).somethingHappened();
      *     } catch (RemoteException e) {
      *         // The RemoteCallbackList will take care of removing
      *         // the dead object for us.
      *     }
      * }
-     * callbacks.finishBroadcast();</pre>
+     * interfaces.finishBroadcast();</pre>
      *
-     * @return Returns the number of callbacks in the broadcast, to be used
+     * Note that this method is only supported for {@link #FROZEN_CALLEE_POLICY_UNSET}. For other
+     * policies use {@link #broadcast(Consumer)} instead.
+     *
+     * @return Returns the number of interfaces in the broadcast, to be used
      * with {@link #getBroadcastItem} to determine the range of indices you
      * can supply.
      *
+     * @throws UnsupportedOperationException if an frozen callee policy is set.
+     *
      * @see #getBroadcastItem
      * @see #finishBroadcast
      */
     public int beginBroadcast() {
-        synchronized (mCallbacks) {
+        if (mFrozenCalleePolicy != FROZEN_CALLEE_POLICY_UNSET) {
+            throw new UnsupportedOperationException();
+        }
+        return beginBroadcastInternal();
+    }
+
+    private int beginBroadcastInternal() {
+        synchronized (mInterfaces) {
             if (mBroadcastCount > 0) {
                 throw new IllegalStateException(
                         "beginBroadcast() called while already in a broadcast");
             }
             
-            final int N = mBroadcastCount = mCallbacks.size();
-            if (N <= 0) {
+            final int n = mBroadcastCount = mInterfaces.size();
+            if (n <= 0) {
                 return 0;
             }
             Object[] active = mActiveBroadcast;
-            if (active == null || active.length < N) {
-                mActiveBroadcast = active = new Object[N];
+            if (active == null || active.length < n) {
+                mActiveBroadcast = active = new Object[n];
             }
-            for (int i=0; i<N; i++) {
-                active[i] = mCallbacks.valueAt(i);
+            for (int i = 0; i < n; i++) {
+                active[i] = mInterfaces.valueAt(i);
             }
-            return N;
+            return n;
         }
     }
 
@@ -267,24 +522,23 @@
      * calling {@link #finishBroadcast}.
      *
      * <p>Note that it is possible for the process of one of the returned
-     * callbacks to go away before you call it, so you will need to catch
+     * interfaces to go away before you call it, so you will need to catch
      * {@link RemoteException} when calling on to the returned object.
-     * The callback list itself, however, will take care of unregistering
+     * The interface list itself, however, will take care of unregistering
      * these objects once it detects that it is no longer valid, so you can
      * handle such an exception by simply ignoring it.
      *
-     * @param index Which of the registered callbacks you would like to
+     * @param index Which of the registered interfaces you would like to
      * retrieve.  Ranges from 0 to {@link #beginBroadcast}-1, inclusive.
      *
-     * @return Returns the callback interface that you can call.  This will
-     * always be non-null.
+     * @return Returns the interface that you can call.  This will always be non-null.
      *
      * @see #beginBroadcast
      */
     public E getBroadcastItem(int index) {
-        return ((Callback)mActiveBroadcast[index]).mCallback;
+        return ((Interface) mActiveBroadcast[index]).mInterface;
     }
-    
+
     /**
      * Retrieve the cookie associated with the item
      * returned by {@link #getBroadcastItem(int)}.
@@ -292,7 +546,7 @@
      * @see #getBroadcastItem
      */
     public Object getBroadcastCookie(int index) {
-        return ((Callback)mActiveBroadcast[index]).mCookie;
+        return ((Interface) mActiveBroadcast[index]).mCookie;
     }
 
     /**
@@ -303,7 +557,7 @@
      * @see #beginBroadcast
      */
     public void finishBroadcast() {
-        synchronized (mCallbacks) {
+        synchronized (mInterfaces) {
             if (mBroadcastCount < 0) {
                 throw new IllegalStateException(
                         "finishBroadcast() called outside of a broadcast");
@@ -322,16 +576,18 @@
     }
 
     /**
-     * Performs {@code action} on each callback, calling
-     * {@link #beginBroadcast()}/{@link #finishBroadcast()} before/after looping
+     * Performs {@code callback} on each registered interface.
      *
-     * @hide
+     * This is equivalent to #beginBroadcast, followed by iterating over the items using
+     * #getBroadcastItem and then @finishBroadcast, except that this method supports
+     * frozen callee policies.
      */
-    public void broadcast(Consumer<E> action) {
-        int itemCount = beginBroadcast();
+    @FlaggedApi(Flags.FLAG_BINDER_FROZEN_STATE_CHANGE_CALLBACK)
+    public void broadcast(@NonNull Consumer<E> callback) {
+        int itemCount = beginBroadcastInternal();
         try {
             for (int i = 0; i < itemCount; i++) {
-                action.accept(getBroadcastItem(i));
+                ((Interface) mActiveBroadcast[i]).addCallback(callback);
             }
         } finally {
             finishBroadcast();
@@ -339,16 +595,16 @@
     }
 
     /**
-     * Performs {@code action} for each cookie associated with a callback, calling
+     * Performs {@code callback} for each cookie associated with an interface, calling
      * {@link #beginBroadcast()}/{@link #finishBroadcast()} before/after looping
      *
      * @hide
      */
-    public <C> void broadcastForEachCookie(Consumer<C> action) {
+    public <C> void broadcastForEachCookie(Consumer<C> callback) {
         int itemCount = beginBroadcast();
         try {
             for (int i = 0; i < itemCount; i++) {
-                action.accept((C) getBroadcastCookie(i));
+                callback.accept((C) getBroadcastCookie(i));
             }
         } finally {
             finishBroadcast();
@@ -356,16 +612,16 @@
     }
 
     /**
-     * Performs {@code action} on each callback and associated cookie, calling {@link
+     * Performs {@code callback} on each interface and associated cookie, calling {@link
      * #beginBroadcast()}/{@link #finishBroadcast()} before/after looping.
      *
      * @hide
      */
-    public <C> void broadcast(BiConsumer<E, C> action) {
+    public <C> void broadcast(BiConsumer<E, C> callback) {
         int itemCount = beginBroadcast();
         try {
             for (int i = 0; i < itemCount; i++) {
-                action.accept(getBroadcastItem(i), (C) getBroadcastCookie(i));
+                callback.accept(getBroadcastItem(i), (C) getBroadcastCookie(i));
             }
         } finally {
             finishBroadcast();
@@ -373,10 +629,10 @@
     }
 
     /**
-     * Returns the number of registered callbacks. Note that the number of registered
-     * callbacks may differ from the value returned by {@link #beginBroadcast()} since
-     * the former returns the number of callbacks registered at the time of the call
-     * and the second the number of callback to which the broadcast will be delivered.
+     * Returns the number of registered interfaces. Note that the number of registered
+     * interfaces may differ from the value returned by {@link #beginBroadcast()} since
+     * the former returns the number of interfaces registered at the time of the call
+     * and the second the number of interfaces to which the broadcast will be delivered.
      * <p>
      * This function is useful to decide whether to schedule a broadcast if this
      * requires doing some work which otherwise would not be performed.
@@ -385,39 +641,39 @@
      * @return The size.
      */
     public int getRegisteredCallbackCount() {
-        synchronized (mCallbacks) {
+        synchronized (mInterfaces) {
             if (mKilled) {
                 return 0;
             }
-            return mCallbacks.size();
+            return mInterfaces.size();
         }
     }
 
     /**
-     * Return a currently registered callback.  Note that this is
+     * Return a currently registered interface.  Note that this is
      * <em>not</em> the same as {@link #getBroadcastItem} and should not be used
-     * interchangeably with it.  This method returns the registered callback at the given
+     * interchangeably with it.  This method returns the registered interface at the given
      * index, not the current broadcast state.  This means that it is not itself thread-safe:
      * any call to {@link #register} or {@link #unregister} will change these indices, so you
      * must do your own thread safety between these to protect from such changes.
      *
-     * @param index Index of which callback registration to return, from 0 to
+     * @param index Index of which interface registration to return, from 0 to
      * {@link #getRegisteredCallbackCount()} - 1.
      *
-     * @return Returns whatever callback is associated with this index, or null if
+     * @return Returns whatever interface is associated with this index, or null if
      * {@link #kill()} has been called.
      */
     public E getRegisteredCallbackItem(int index) {
-        synchronized (mCallbacks) {
+        synchronized (mInterfaces) {
             if (mKilled) {
                 return null;
             }
-            return mCallbacks.valueAt(index).mCallback;
+            return mInterfaces.valueAt(index).mInterface;
         }
     }
 
     /**
-     * Return any cookie associated with a currently registered callback.  Note that this is
+     * Return any cookie associated with a currently registered interface.  Note that this is
      * <em>not</em> the same as {@link #getBroadcastCookie} and should not be used
      * interchangeably with it.  This method returns the current cookie registered at the given
      * index, not the current broadcast state.  This means that it is not itself thread-safe:
@@ -431,25 +687,25 @@
      * {@link #kill()} has been called.
      */
     public Object getRegisteredCallbackCookie(int index) {
-        synchronized (mCallbacks) {
+        synchronized (mInterfaces) {
             if (mKilled) {
                 return null;
             }
-            return mCallbacks.valueAt(index).mCookie;
+            return mInterfaces.valueAt(index).mCookie;
         }
     }
 
     /** @hide */
     public void dump(PrintWriter pw, String prefix) {
-        synchronized (mCallbacks) {
-            pw.print(prefix); pw.print("callbacks: "); pw.println(mCallbacks.size());
+        synchronized (mInterfaces) {
+            pw.print(prefix); pw.print("callbacks: "); pw.println(mInterfaces.size());
             pw.print(prefix); pw.print("killed: "); pw.println(mKilled);
             pw.print(prefix); pw.print("broadcasts count: "); pw.println(mBroadcastCount);
         }
     }
 
-    private void logExcessiveCallbacks() {
-        final long size = mCallbacks.size();
+    private void logExcessiveInterfaces() {
+        final long size = mInterfaces.size();
         final long TOO_MANY = 3000;
         final long MAX_CHARS = 1000;
         if (size >= TOO_MANY) {
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 81987907..c7cc653 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -2,87 +2,7 @@
 container: "system"
 container: "system"
 
-flag {
-    name: "android_os_build_vanilla_ice_cream"
-    is_exported: true
-    namespace: "build"
-    description: "Feature flag for adding the VANILLA_ICE_CREAM constant."
-    bug: "264658905"
-}
-
-flag {
-    name: "state_of_health_public"
-    is_exported: true
-    namespace: "system_sw_battery"
-    description: "Feature flag for making state_of_health a public api."
-    bug: "288842045"
-}
-
-flag {
-    name: "disallow_cellular_null_ciphers_restriction"
-    namespace: "cellular_security"
-    description: "Guards a new UserManager user restriction that admins can use to require cellular encryption on their managed devices."
-    bug: "276752881"
-}
-
-flag {
-    name: "remove_app_profiler_pss_collection"
-    is_exported: true
-    namespace: "backstage_power"
-    description: "Replaces background PSS collection in AppProfiler with RSS"
-    bug: "297542292"
-}
-
-flag {
-    name: "allow_thermal_headroom_thresholds"
-    is_exported: true
-    namespace: "game"
-    description: "Enable thermal headroom thresholds API"
-    bug: "288119641"
-}
-
-# This flag guards the private space feature, its APIs, and some of the feature implementations. The flag android.multiuser.Flags.enable_private_space_features exclusively guards all the implementations.
-flag {
-    name: "allow_private_profile"
-    is_exported: true
-    namespace: "profile_experiences"
-    description: "Guards a new Private Profile type in UserManager - everything from its setup to config to deletion."
-    bug: "299069460"
-    is_exported: true
-}
-
-flag {
-    name: "adpf_prefer_power_efficiency"
-    is_exported: true
-    namespace: "game"
-    description: "Guards the ADPF power efficiency API"
-    bug: "288117936"
-}
-
-flag {
-    name: "security_state_service"
-    is_exported: true
-    namespace: "dynamic_spl"
-    description: "Guards the Security State API."
-    bug: "302189431"
-}
-
-flag {
-    name: "ordered_broadcast_multiple_permissions"
-    is_exported: true
-    namespace: "bluetooth"
-    description: "Guards the Context.sendOrderedBroadcastMultiplePermissions API"
-    bug: "345802719"
-}
-
-flag {
-    name: "battery_saver_supported_check_api"
-    is_exported: true
-    namespace: "backstage_power"
-    description: "Guards a new API in PowerManager to check if battery saver is supported or not."
-    bug: "305067031"
-}
-
+# keep-sorted start block=yes newline_separated=yes
 flag {
     name: "adpf_gpu_report_actual_work_duration"
     is_exported: true
@@ -92,21 +12,6 @@
 }
 
 flag {
-    name: "adpf_use_fmq_channel"
-    namespace: "game"
-    description: "Guards use of the FMQ channel for ADPF"
-    bug: "315894228"
-}
-
-flag {
-    name: "adpf_use_fmq_channel_fixed"
-    namespace: "game"
-    description: "Guards use of the FMQ channel for ADPF with a readonly flag"
-    is_fixed_read_only: true
-    bug: "315894228"
-}
-
-flag {
     name: "adpf_hwui_gpu"
     namespace: "game"
     description: "Guards use of the FMQ channel for ADPF"
@@ -115,6 +20,13 @@
 }
 
 flag {
+    name: "adpf_measure_during_input_event_boost"
+    namespace: "game"
+    description: "Guards use of a boost when view measures during input events"
+    bug: "256549451"
+}
+
+flag {
     name: "adpf_obtainview_boost"
     namespace: "game"
     description: "Guards use of a boost in response to HWUI obtainView"
@@ -131,41 +43,67 @@
 }
 
 flag {
-    name: "adpf_measure_during_input_event_boost"
-    namespace: "game"
-    description: "Guards use of a boost when view measures during input events"
-    bug: "256549451"
-}
-
-flag {
-    name: "battery_service_support_current_adb_command"
-    namespace: "backstage_power"
-    description: "Whether or not BatteryService supports adb commands for Current values."
-    is_fixed_read_only: true
-    bug: "315037695"
-}
-
-flag {
-    name: "strict_mode_restricted_network"
-    namespace: "backstage_power"
-    description: "Guards StrictMode APIs for detecting restricted network access."
-    bug: "317250784"
-}
-
-flag {
-    name: "binder_frozen_state_change_callback"
+    name: "adpf_prefer_power_efficiency"
     is_exported: true
-    namespace: "system_performance"
-    description: "Guards the frozen state change callback API."
-    bug: "361157077"
+    namespace: "game"
+    description: "Guards the ADPF power efficiency API"
+    bug: "288117936"
 }
 
 flag {
-    name: "message_queue_tail_tracking"
-    namespace: "system_performance"
-    description: "track tail of message queue."
-    bug: "305311707"
+    name: "adpf_use_fmq_channel"
+    namespace: "game"
+    description: "Guards use of the FMQ channel for ADPF"
+    bug: "315894228"
+}
+
+flag {
+    name: "adpf_use_fmq_channel_fixed"
+    namespace: "game"
+    description: "Guards use of the FMQ channel for ADPF with a readonly flag"
     is_fixed_read_only: true
+    bug: "315894228"
+}
+
+flag {
+    name: "allow_consentless_bugreport_delegated_consent"
+    namespace: "crumpet"
+    description: "Allow privileged apps to call bugreport generation without enforcing user consent and delegate it to the calling app instead"
+    bug: "324046728"
+}
+
+# This flag guards the private space feature, its APIs, and some of the feature implementations. The flag android.multiuser.Flags.enable_private_space_features exclusively guards all the implementations.
+flag {
+    name: "allow_private_profile"
+    is_exported: true
+    namespace: "profile_experiences"
+    description: "Guards a new Private Profile type in UserManager - everything from its setup to config to deletion."
+    bug: "299069460"
+    is_exported: true
+}
+
+flag {
+    name: "allow_thermal_headroom_thresholds"
+    is_exported: true
+    namespace: "game"
+    description: "Enable thermal headroom thresholds API"
+    bug: "288119641"
+}
+
+flag {
+    name: "android_os_build_vanilla_ice_cream"
+    is_exported: true
+    namespace: "build"
+    description: "Feature flag for adding the VANILLA_ICE_CREAM constant."
+    bug: "264658905"
+}
+
+flag {
+    name: "api_for_backported_fixes"
+    namespace: "media_reliability"
+    description: "Public API app developers use to check if a known issue is fixed on a device."
+    bug: "308461809"
+    is_exported: true
 }
 
 flag {
@@ -178,35 +116,42 @@
 }
 
 flag {
-    name: "storage_lifetime_api"
+    name: "battery_saver_supported_check_api"
     is_exported: true
-    namespace: "phoenix"
-    description: "Feature flag for adding storage component health APIs."
+    namespace: "backstage_power"
+    description: "Guards a new API in PowerManager to check if battery saver is supported or not."
+    bug: "305067031"
+}
+
+flag {
+    name: "battery_service_support_current_adb_command"
+    namespace: "backstage_power"
+    description: "Whether or not BatteryService supports adb commands for Current values."
     is_fixed_read_only: true
-    bug: "309792384"
+    bug: "315037695"
 }
 
 flag {
-     namespace: "system_performance"
-     name: "telemetry_apis_framework_initialization"
-     is_exported: true
-     description: "Control framework initialization APIs of telemetry APIs feature."
-     is_fixed_read_only: true
-     bug: "324241334"
+    name: "binder_frozen_state_change_callback"
+    is_exported: true
+    namespace: "system_performance"
+    description: "Guards the frozen state change callback API."
+    bug: "361157077"
 }
 
 flag {
-     namespace: "system_performance"
-     name: "perfetto_sdk_tracing"
-     description: "Tracing using Perfetto SDK."
-     bug: "303199244"
+    name: "disallow_cellular_null_ciphers_restriction"
+    namespace: "cellular_security"
+    description: "Guards a new UserManager user restriction that admins can use to require cellular encryption on their managed devices."
+    bug: "276752881"
 }
 
 flag {
-    name: "allow_consentless_bugreport_delegated_consent"
-    namespace: "crumpet"
-    description: "Allow privileged apps to call bugreport generation without enforcing user consent and delegate it to the calling app instead"
-    bug: "324046728"
+    name: "enable_angle_allow_list"
+    namespace: "gpu"
+    description: "Whether to read from angle allowlist to determine if app should use ANGLE"
+    is_fixed_read_only: true
+    bug: "370845648"
 }
 
 flag {
@@ -226,6 +171,14 @@
 }
 
 flag {
+    name: "message_queue_tail_tracking"
+    namespace: "system_performance"
+    description: "track tail of message queue."
+    bug: "305311707"
+    is_fixed_read_only: true
+}
+
+flag {
     name: "network_time_uses_shared_memory"
     namespace: "system_performance"
     description: "SystemClock.currentNetworkTimeMillis() reads network time offset from shared memory"
@@ -234,17 +187,67 @@
 }
 
 flag {
-    name: "enable_angle_allow_list"
-    namespace: "gpu"
-    description: "Whether to read from angle allowlist to determine if app should use ANGLE"
-    is_fixed_read_only: true
-    bug: "370845648"
+    name: "ordered_broadcast_multiple_permissions"
+    is_exported: true
+    namespace: "bluetooth"
+    description: "Guards the Context.sendOrderedBroadcastMultiplePermissions API"
+    bug: "345802719"
 }
 
 flag {
-    name: "api_for_backported_fixes"
-    namespace: "media_reliability"
-    description: "Public API app developers use to check if a known issue is fixed on a device."
-    bug: "308461809"
+    name: "remove_app_profiler_pss_collection"
     is_exported: true
+    namespace: "backstage_power"
+    description: "Replaces background PSS collection in AppProfiler with RSS"
+    bug: "297542292"
 }
+
+flag {
+    name: "security_state_service"
+    is_exported: true
+    namespace: "dynamic_spl"
+    description: "Guards the Security State API."
+    bug: "302189431"
+}
+
+flag {
+    name: "state_of_health_public"
+    is_exported: true
+    namespace: "system_sw_battery"
+    description: "Feature flag for making state_of_health a public api."
+    bug: "288842045"
+}
+
+flag {
+    name: "storage_lifetime_api"
+    is_exported: true
+    namespace: "phoenix"
+    description: "Feature flag for adding storage component health APIs."
+    is_fixed_read_only: true
+    bug: "309792384"
+}
+
+flag {
+    name: "strict_mode_restricted_network"
+    namespace: "backstage_power"
+    description: "Guards StrictMode APIs for detecting restricted network access."
+    bug: "317250784"
+}
+
+flag {
+     namespace: "system_performance"
+     name: "perfetto_sdk_tracing"
+     description: "Tracing using Perfetto SDK."
+     bug: "303199244"
+}
+
+flag {
+     namespace: "system_performance"
+     name: "telemetry_apis_framework_initialization"
+     is_exported: true
+     description: "Control framework initialization APIs of telemetry APIs feature."
+     is_fixed_read_only: true
+     bug: "324241334"
+}
+
+# keep-sorted end
diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp
index 06621c9..8bd7078 100644
--- a/core/jni/platform/host/HostRuntime.cpp
+++ b/core/jni/platform/host/HostRuntime.cpp
@@ -430,7 +430,6 @@
 
 } // namespace android
 
-#ifndef _WIN32
 using namespace android;
 
 JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
@@ -439,12 +438,14 @@
         return JNI_ERR;
     }
 
-    Vector<String8> args;
-    HostRuntime runtime;
+    string useBaseHostRuntime = getJavaProperty(env, "use_base_native_hostruntime");
+    if (useBaseHostRuntime == "true") {
+        Vector<String8> args;
+        HostRuntime runtime;
 
-    runtime.onVmCreated(env);
-    runtime.start("HostRuntime", args, false);
+        runtime.onVmCreated(env);
+        runtime.start("HostRuntime", args, false);
+    }
 
     return JNI_VERSION_1_6;
 }
-#endif
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
index 7f1e4a8..61cd1c3 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
@@ -170,7 +170,7 @@
             SNAP_TO_NONE,
             SNAP_TO_START_AND_DISMISS,
             SNAP_TO_END_AND_DISMISS,
-            SNAP_TO_MINIMIZE
+            SNAP_TO_MINIMIZE,
     })
     public @interface SnapPosition {}
 
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 92535f3..57a59c9 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
@@ -521,7 +521,6 @@
         wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED)
 
         transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
-
     }
 
     private fun exitSplitIfApplicable(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) {
@@ -663,7 +662,6 @@
         wct.reparent(task.token, displayAreaInfo.token, true /* onTop */)
 
         transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
-
     }
 
     /** Moves a task in/out of full immersive state within the desktop. */
@@ -739,7 +737,6 @@
         val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
 
         toggleResizeDesktopTaskTransitionHandler.startTransition(wct)
-
     }
 
     private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect {
@@ -851,7 +848,6 @@
         val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
 
         toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentDragBounds)
-
     }
 
     @VisibleForTesting
@@ -1246,10 +1242,23 @@
                 error("Invalid windowing mode: ${callingTask.windowingMode}")
             }
         }
+        val bounds = when (newTaskWindowingMode) {
+            WINDOWING_MODE_FREEFORM -> {
+                displayController.getDisplayLayout(callingTask.displayId)
+                    ?.let { getInitialBounds(it, callingTask) }
+            }
+            WINDOWING_MODE_MULTI_WINDOW -> {
+                Rect()
+            }
+            else -> {
+                error("Invalid windowing mode: $newTaskWindowingMode")
+            }
+        }
         return ActivityOptions.makeBasic().apply {
             launchWindowingMode = newTaskWindowingMode
             pendingIntentBackgroundActivityStartMode =
                 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
+            launchBounds = bounds
         }
     }
 
@@ -1405,15 +1414,7 @@
             } else {
                 WINDOWING_MODE_FREEFORM
             }
-        val initialBounds = if (ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue()) {
-            calculateInitialBounds(displayLayout, taskInfo)
-        } else {
-            getDefaultDesktopTaskBounds(displayLayout)
-        }
-
-        if (DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue()) {
-            cascadeWindow(taskInfo, initialBounds, displayLayout)
-        }
+        val initialBounds = getInitialBounds(displayLayout, taskInfo)
 
         if (canChangeTaskPosition(taskInfo)) {
             wct.setBounds(taskInfo.token, initialBounds)
@@ -1425,6 +1426,22 @@
         }
     }
 
+    private fun getInitialBounds(
+        displayLayout: DisplayLayout,
+        taskInfo: RunningTaskInfo
+    ): Rect {
+        val bounds = if (ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue) {
+            calculateInitialBounds(displayLayout, taskInfo)
+        } else {
+            getDefaultDesktopTaskBounds(displayLayout)
+        }
+
+        if (DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue) {
+            cascadeWindow(taskInfo, bounds, displayLayout)
+        }
+        return bounds
+    }
+
     private fun addMoveToFullscreenChanges(
         wct: WindowContainerTransaction,
         taskInfo: RunningTaskInfo
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index dfa2437..5c72cb7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -23,6 +23,7 @@
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
 
+import static com.android.wm.shell.Flags.enableFlexibleSplit;
 import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_BOTTOM;
 import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_LEFT;
 import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_RIGHT;
@@ -43,14 +44,18 @@
 import android.graphics.Insets;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.graphics.Region;
 import android.graphics.drawable.Drawable;
+import android.util.Log;
 import android.view.DragEvent;
 import android.view.SurfaceControl;
+import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
 import android.view.WindowInsets.Type;
+import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.window.WindowContainerToken;
 
@@ -67,14 +72,22 @@
 import com.android.wm.shell.splitscreen.SplitScreenController;
 
 import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.BiConsumer;
 
 /**
  * Coordinates the visible drop targets for the current drag within a single display.
  */
 public class DragLayout extends LinearLayout
-        implements ViewTreeObserver.OnComputeInternalInsetsListener, DragLayoutProvider {
+        implements ViewTreeObserver.OnComputeInternalInsetsListener, DragLayoutProvider,
+        DragZoneAnimator{
 
+    static final boolean DEBUG_LAYOUT = false;
     // While dragging the status bar is hidden.
     private static final int HIDE_STATUS_BAR_FLAGS = StatusBarManager.DISABLE_NOTIFICATION_ICONS
             | StatusBarManager.DISABLE_NOTIFICATION_ALERTS
@@ -108,13 +121,19 @@
     // The last position that was handled by the drag layout
     private final Point mLastPosition = new Point();
 
+    // Used with enableFlexibleSplit() flag
+    private List<SplitDragPolicy.Target> mTargets;
+    private Map<SplitDragPolicy.Target, DropZoneView> mTargetDropMap = new HashMap<>();
+    private FrameLayout mAnimatingRootLayout;
+    // Used with enableFlexibleSplit() flag
+
     @SuppressLint("WrongConstant")
     public DragLayout(Context context, SplitScreenController splitScreenController,
             IconProvider iconProvider) {
         super(context);
         mSplitScreenController = splitScreenController;
         mIconProvider = iconProvider;
-        mPolicy = new SplitDragPolicy(context, splitScreenController);
+        mPolicy = new SplitDragPolicy(context, splitScreenController, this);
         mStatusBarManager = context.getSystemService(StatusBarManager.class);
         mLastConfiguration.setTo(context.getResources().getConfiguration());
 
@@ -211,11 +230,26 @@
         boolean isLeftRightSplit = mSplitScreenController != null
                 && mSplitScreenController.isLeftRightSplit();
         if (isLeftRightSplit) {
-            mDropZoneView1.setBottomInset(mInsets.bottom);
-            mDropZoneView2.setBottomInset(mInsets.bottom);
+            if (enableFlexibleSplit()) {
+                mTargetDropMap.values().forEach(dzv -> dzv.setBottomInset(mInsets.bottom));
+            } else {
+                mDropZoneView1.setBottomInset(mInsets.bottom);
+                mDropZoneView2.setBottomInset(mInsets.bottom);
+            }
         } else {
-            mDropZoneView1.setBottomInset(0);
-            mDropZoneView2.setBottomInset(mInsets.bottom);
+            if (enableFlexibleSplit()) {
+                Collection<DropZoneView> dropViews = mTargetDropMap.values();
+                final DropZoneView[] bottomView = {null};
+                dropViews.forEach(dropZoneView -> {
+                    bottomView[0] = dropZoneView;
+                    dropZoneView.setBottomInset(0);
+                });
+                // TODO(b/349828130): necessary? maybe with UI polish
+                //  bottomView[0].setBottomInset(mInsets.bottom);
+            } else {
+                mDropZoneView1.setBottomInset(0);
+                mDropZoneView2.setBottomInset(mInsets.bottom);
+            }
         }
         return super.onApplyWindowInsets(insets);
     }
@@ -233,17 +267,31 @@
         final boolean themeChanged = (diff & CONFIG_ASSETS_PATHS) != 0
                 || (diff & CONFIG_UI_MODE) != 0;
         if (themeChanged) {
-            mDropZoneView1.onThemeChange();
-            mDropZoneView2.onThemeChange();
+            if (enableFlexibleSplit()) {
+                mTargetDropMap.values().forEach(DropZoneView::onThemeChange);
+            } else {
+                mDropZoneView1.onThemeChange();
+                mDropZoneView2.onThemeChange();
+            }
         }
         mLastConfiguration.setTo(newConfig);
         requestLayout();
     }
 
     private void updateContainerMarginsForSingleTask() {
-        mDropZoneView1.setContainerMargin(
-                mDisplayMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin);
-        mDropZoneView2.setContainerMargin(0, 0, 0, 0);
+        if (enableFlexibleSplit()) {
+            DropZoneView firstDropZone = mTargetDropMap.values().stream().findFirst().get();
+            mTargetDropMap.values().stream()
+                    .filter(dropZoneView -> dropZoneView != firstDropZone)
+                    .forEach(dropZoneView -> dropZoneView.setContainerMargin(0, 0, 0, 0));
+            firstDropZone.setContainerMargin(
+                    mDisplayMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin
+            );
+        } else {
+            mDropZoneView1.setContainerMargin(
+                    mDisplayMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin);
+            mDropZoneView2.setContainerMargin(0, 0, 0, 0);
+        }
     }
 
     private void updateContainerMargins(boolean isLeftRightSplit) {
@@ -306,19 +354,35 @@
                 }
             }
         } else {
-            // We're already in split so get taskInfo from the controller to populate icon / color.
-            ActivityManager.RunningTaskInfo topOrLeftTask =
-                    mSplitScreenController.getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
-            ActivityManager.RunningTaskInfo bottomOrRightTask =
-                    mSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
-            if (topOrLeftTask != null && bottomOrRightTask != null) {
-                Drawable topOrLeftIcon = mIconProvider.getIcon(topOrLeftTask.topActivityInfo);
-                int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask);
-                Drawable bottomOrRightIcon = mIconProvider.getIcon(
-                        bottomOrRightTask.topActivityInfo);
-                int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask);
-                mDropZoneView1.setAppInfo(topOrLeftColor, topOrLeftIcon);
-                mDropZoneView2.setAppInfo(bottomOrRightColor, bottomOrRightIcon);
+            ActivityManager.RunningTaskInfo[] taskInfos = mSplitScreenController.getAllTaskInfos();
+            boolean anyTasksNull = Arrays.stream(taskInfos).anyMatch(Objects::isNull);
+            if (enableFlexibleSplit() && taskInfos != null && !anyTasksNull) {
+                int i = 0;
+                for (DropZoneView v : mTargetDropMap.values()) {
+                    if (i >= taskInfos.length) {
+                        // TODO(b/349828130) Support once we add 3 StageRoots
+                        continue;
+                    }
+                    ActivityManager.RunningTaskInfo task = taskInfos[i];
+                    v.setAppInfo(getResizingBackgroundColor(task),
+                            mIconProvider.getIcon(task.topActivityInfo));
+                    i++;
+                }
+            } else {
+                // We're already in split so get taskInfo from the controller to populate icon / color.
+                ActivityManager.RunningTaskInfo topOrLeftTask =
+                        mSplitScreenController.getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+                ActivityManager.RunningTaskInfo bottomOrRightTask =
+                        mSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+                if (topOrLeftTask != null && bottomOrRightTask != null) {
+                    Drawable topOrLeftIcon = mIconProvider.getIcon(topOrLeftTask.topActivityInfo);
+                    int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask);
+                    Drawable bottomOrRightIcon = mIconProvider.getIcon(
+                            bottomOrRightTask.topActivityInfo);
+                    int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask);
+                    mDropZoneView1.setAppInfo(topOrLeftColor, topOrLeftIcon);
+                    mDropZoneView2.setAppInfo(bottomOrRightColor, bottomOrRightIcon);
+                }
             }
 
             // Update the dropzones to match existing split sizes
@@ -391,7 +455,14 @@
     @NonNull
     @Override
     public void addDraggingView(ViewGroup rootView) {
-        // TODO(b/349828130) We need to separate out view + logic here
+        if (enableFlexibleSplit()) {
+            removeAllViews();
+            mAnimatingRootLayout = new FrameLayout(getContext());
+            addView(mAnimatingRootLayout,
+                    new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+            ((LayoutParams) mAnimatingRootLayout.getLayoutParams()).weight = 1;
+        }
+
         rootView.addView(this, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
     }
 
@@ -409,6 +480,24 @@
             // Inset the draw region by a little bit
             target.drawRegion.inset(mDisplayMargin, mDisplayMargin);
         }
+
+        if (enableFlexibleSplit()) {
+            mTargets = targets;
+            mTargetDropMap.clear();
+            for (int i = 0; i < mTargets.size(); i++) {
+                DropZoneView v = new DropZoneView(getContext());
+                SplitDragPolicy.Target t = mTargets.get(i);
+                FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(t.drawRegion.width(),
+                        t.drawRegion.height());
+                mAnimatingRootLayout.addView(v, params);
+                v.setTranslationX(t.drawRegion.left);
+                v.setTranslationY(t.drawRegion.top);
+                mTargetDropMap.put(t, v);
+                if (DEBUG_LAYOUT) {
+                    v.setDebugIndex(t.index);
+                }
+            }
+        }
     }
 
     /**
@@ -433,6 +522,9 @@
             if (target == null) {
                 // Animating to no target
                 animateSplitContainers(false, null /* animCompleteCallback */);
+                if (enableFlexibleSplit()) {
+                    animateHighlight(target);
+                }
             } else if (mCurrentTarget == null) {
                 if (mPolicy.getNumTargets() == 1) {
                     animateFullscreenContainer(true);
@@ -440,10 +532,14 @@
                     animateSplitContainers(true, null /* animCompleteCallback */);
                     animateHighlight(target);
                 }
-            } else if (mCurrentTarget.type != target.type) {
+            } else if (mCurrentTarget.type != target.type || enableFlexibleSplit()) {
                 // Switching between targets
-                mDropZoneView1.animateSwitch();
-                mDropZoneView2.animateSwitch();
+                if (enableFlexibleSplit()) {
+                    animateHighlight(target);
+                } else {
+                    mDropZoneView1.animateSwitch();
+                    mDropZoneView2.animateSwitch();
+                }
                 // Announce for accessibility.
                 switch (target.type) {
                     case TYPE_SPLIT_LEFT:
@@ -490,6 +586,9 @@
         mDropZoneView2.setForceIgnoreBottomMargin(false);
         updateContainerMargins(mIsLeftRightSplit);
         mCurrentTarget = null;
+        if (enableFlexibleSplit()) {
+            mAnimatingRootLayout.removeAllViews();
+        }
     }
 
     /**
@@ -566,9 +665,20 @@
         mStatusBarManager.disable(visible
                 ? HIDE_STATUS_BAR_FLAGS
                 : DISABLE_NONE);
-        mDropZoneView1.setShowingMargin(visible);
-        mDropZoneView2.setShowingMargin(visible);
-        Animator animator = mDropZoneView1.getAnimator();
+        Animator animator;
+        if (enableFlexibleSplit()) {
+            DropZoneView anyDropZoneView = null;
+            for (DropZoneView dz : mTargetDropMap.values()) {
+                dz.setShowingMargin(visible);
+                anyDropZoneView = dz;
+            }
+            animator = anyDropZoneView != null ? anyDropZoneView.getAnimator() : null;
+        } else {
+            mDropZoneView1.setShowingMargin(visible);
+            mDropZoneView2.setShowingMargin(visible);
+            animator = mDropZoneView1.getAnimator();
+        }
+
         if (animCompleteCallback != null) {
             if (animator != null) {
                 animator.addListener(new AnimatorListenerAdapter() {
@@ -584,7 +694,24 @@
         }
     }
 
+    @Override
+    public void animateDragTargets(
+            @NonNull List<? extends BiConsumer<SplitDragPolicy.Target, View>> viewsToAnimate) {
+        for (Map.Entry<SplitDragPolicy.Target, DropZoneView> entry : mTargetDropMap.entrySet()) {
+            viewsToAnimate.get(0).accept(entry.getKey(), entry.getValue());
+        }
+    }
+
     private void animateHighlight(SplitDragPolicy.Target target) {
+        if (enableFlexibleSplit()) {
+            for (Map.Entry<SplitDragPolicy.Target, DropZoneView> dzv : mTargetDropMap.entrySet()) {
+                // Highlight the view w/ the matching target, unhighlight the rest
+                dzv.getValue().setShowingHighlight(dzv.getKey() == target);
+            }
+            mPolicy.onHoveringOver(target);
+            return;
+        }
+
         if (target.type == TYPE_SPLIT_LEFT || target.type == TYPE_SPLIT_TOP) {
             mDropZoneView1.setShowingHighlight(true);
             mDropZoneView2.setShowingHighlight(false);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragZoneAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragZoneAnimator.kt
new file mode 100644
index 0000000..240465d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragZoneAnimator.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.draganddrop
+
+import android.view.View
+import java.util.function.BiConsumer
+
+interface DragZoneAnimator {
+    /**
+     * Each consumer will be called for the corresponding DropZoneView.
+     * This must match the number of targets in [.mTargets] otherwise will
+     * throw an [IllegalStateException]
+     */
+    fun animateDragTargets(viewsToAnimate: List<BiConsumer<SplitDragPolicy.Target, View>>)
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt
index 122a105..2bbca48 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt
@@ -47,7 +47,7 @@
     /**
      * Called when user is hovering Drag object over the given Target
      */
-    fun onHoveringOver(target: SplitDragPolicy.Target) {}
+    fun onHoveringOver(target: SplitDragPolicy.Target?) {}
     /**
      * Called when the user has dropped the provided target (need not be the same target as
      * [onHoveringOver])
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
index f9749ec..e503b8c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
@@ -20,12 +20,14 @@
 
 import android.animation.Animator;
 import android.animation.ObjectAnimator;
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Path;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.view.Gravity;
@@ -33,6 +35,7 @@
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
+import android.widget.TextView;
 
 import androidx.annotation.Nullable;
 
@@ -83,6 +86,7 @@
     private int mTargetBackgroundColor;
     private ObjectAnimator mMarginAnimator;
     private float mMarginPercent;
+    private TextView mDebugIndex;
 
     // Renders a highlight or neutral transparent color
     private ColorDrawable mColorDrawable;
@@ -125,6 +129,22 @@
         mMarginView = new MarginView(context);
         addView(mMarginView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                 ViewGroup.LayoutParams.MATCH_PARENT));
+
+        if (DEBUG_LAYOUT) {
+            mDebugIndex = new TextView(context);
+            mDebugIndex.setVisibility(GONE);
+            mDebugIndex.setTextColor(Color.YELLOW);
+            addView(mDebugIndex, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                    ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.START | Gravity.TOP));
+
+            View borderView = new View(context);
+            addView(borderView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.MATCH_PARENT));
+            GradientDrawable border = new GradientDrawable();
+            border.setShape(GradientDrawable.RECTANGLE);
+            border.setStroke(5, Color.RED);
+            borderView.setBackground(border);
+        }
     }
 
     public void onThemeChange() {
@@ -236,6 +256,16 @@
         }
     }
 
+    @SuppressLint("SetTextI18n")
+    public void setDebugIndex(int index) {
+        if (!DEBUG_LAYOUT) {
+            return;
+        }
+
+        mDebugIndex.setText("Index:\n" + index);
+        mDebugIndex.setVisibility(VISIBLE);
+    }
+
     private void animateBackground(int startColor, int endColor) {
         if (DEBUG_LAYOUT) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java
index 2a19d65..5d22c1e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java
@@ -32,16 +32,22 @@
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 
+import static com.android.wm.shell.Flags.enableFlexibleSplit;
+import static com.android.wm.shell.draganddrop.DragLayout.DEBUG_LAYOUT;
 import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_FULLSCREEN;
 import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_BOTTOM;
 import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_LEFT;
 import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_RIGHT;
 import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_TOP;
 import static com.android.wm.shell.shared.draganddrop.DragAndDropConstants.EXTRA_DISALLOW_HIT_REGION;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
 
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
 import android.app.PendingIntent;
@@ -59,6 +65,7 @@
 import android.os.UserHandle;
 import android.util.Log;
 import android.util.Slog;
+import android.view.View;
 import android.window.WindowContainerToken;
 
 import androidx.annotation.IntDef;
@@ -69,13 +76,23 @@
 import com.android.internal.logging.InstanceId;
 import com.android.internal.protolog.ProtoLog;
 import com.android.wm.shell.R;
+import com.android.wm.shell.draganddrop.anim.DropTargetAnimSupplier;
+import com.android.wm.shell.draganddrop.anim.HoverAnimProps;
+import com.android.wm.shell.draganddrop.anim.TwoFiftyFiftyTargetAnimator;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.split.SplitScreenConstants;
 import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import kotlin.Pair;
 
 /**
  * The policy for handling drag and drop operations to shell.
@@ -89,24 +106,42 @@
     private final Starter mFullscreenStarter;
     // Used for launching tasks into splitscreen
     private final Starter mSplitscreenStarter;
+    private final DragZoneAnimator mDragZoneAnimator;
     private final SplitScreenController mSplitScreen;
-    private final ArrayList<SplitDragPolicy.Target> mTargets = new ArrayList<>();
+    private ArrayList<SplitDragPolicy.Target> mTargets = new ArrayList<>();
     private final RectF mDisallowHitRegion = new RectF();
+    /**
+     * Maps a given SnapPosition to an array where each index of the array represents one
+     * of the targets that are being hovered over, in order (Left to Right, Top to Bottom).
+     * Ex: 4 drop targets when we're in 50/50 split
+     * 2_50_50 => [ [AnimPropsTarget1, AnimPropsTarget2, AnimPropsTarget3, AnimPropsTarget4],
+     *              ... // hovering over target 2,
+     *              ... // hovering over target 3,
+     *              ... // hovering over target 4
+     *            ]
+     */
+    private final Map<Integer, List<List<HoverAnimProps>>> mHoverAnimProps = new HashMap();
 
     private InstanceId mLoggerSessionId;
     private DragSession mSession;
+    @Nullable
+    private Target mCurrentHoverTarget;
+    /** This variable is a temporary placeholder, will be queried on drag start. */
+    private int mCurrentSnapPosition = -1;
 
-    public SplitDragPolicy(Context context, SplitScreenController splitScreen) {
-        this(context, splitScreen, new DefaultStarter(context));
+    public SplitDragPolicy(Context context, SplitScreenController splitScreen,
+            DragZoneAnimator dragZoneAnimator) {
+        this(context, splitScreen, new DefaultStarter(context), dragZoneAnimator);
     }
 
     @VisibleForTesting
     SplitDragPolicy(Context context, SplitScreenController splitScreen,
-            Starter fullscreenStarter) {
+            Starter fullscreenStarter, DragZoneAnimator dragZoneAnimator) {
         mContext = context;
         mSplitScreen = splitScreen;
         mFullscreenStarter = fullscreenStarter;
         mSplitscreenStarter = splitScreen;
+        mDragZoneAnimator = dragZoneAnimator;
     }
 
     /**
@@ -164,58 +199,123 @@
                 || (mSession.runningTaskActType == ACTIVITY_TYPE_STANDARD
                         && mSession.runningTaskWinMode == WINDOWING_MODE_FULLSCREEN);
         if (allowSplit) {
-            // Already split, allow replacing existing split task
-            final Rect topOrLeftBounds = new Rect();
-            final Rect bottomOrRightBounds = new Rect();
-            mSplitScreen.getStageBounds(topOrLeftBounds, bottomOrRightBounds);
-            topOrLeftBounds.intersect(displayRegion);
-            bottomOrRightBounds.intersect(displayRegion);
+            if (enableFlexibleSplit()) {
+                // TODO(b/349828130) get this from split screen controller, expose the SnapTarget object
+                //  entirely and then pull out the SnapPosition
+                @SplitScreenConstants.SnapPosition int snapPosition = SNAP_TO_2_50_50;
+                final Rect startHitRegion = new Rect();
+                final Rect endHitRegion = new Rect();
+                if (!inSplitScreen) {
+                    // Currently in fullscreen, split in half
+                    final Rect startBounds = new Rect();
+                    final Rect endBounds = new Rect();
+                    mSplitScreen.getStageBounds(startBounds, endBounds);
+                    startBounds.intersect(displayRegion);
+                    endBounds.intersect(displayRegion);
 
-            if (isLeftRightSplit) {
-                final Rect leftHitRegion = new Rect();
-                final Rect rightHitRegion = new Rect();
+                    if (isLeftRightSplit) {
+                        displayRegion.splitVertically(startHitRegion, endHitRegion);
+                    } else {
+                        displayRegion.splitHorizontally(startHitRegion, endHitRegion);
+                    }
 
-                // If we have existing split regions use those bounds, otherwise split it 50/50
-                if (inSplitScreen) {
-                    // The bounds of the existing split will have a divider bar, the hit region
-                    // should include that space. Find the center of the divider bar:
-                    float centerX = topOrLeftBounds.right + (dividerWidth / 2);
-                    // Now set the hit regions using that center.
-                    leftHitRegion.set(displayRegion);
-                    leftHitRegion.right = (int) centerX;
-                    rightHitRegion.set(displayRegion);
-                    rightHitRegion.left = (int) centerX;
+                    mTargets.add(new Target(TYPE_SPLIT_LEFT, startHitRegion, startBounds, -1));
+                    mTargets.add(new Target(TYPE_SPLIT_RIGHT, endHitRegion, endBounds, -1));
                 } else {
-                    displayRegion.splitVertically(leftHitRegion, rightHitRegion);
+                    // TODO(b/349828130), move this into init function and/or the insets updating
+                    //  callback
+                    DropTargetAnimSupplier supplier = null;
+                    switch (snapPosition) {
+                        case SNAP_TO_2_50_50:
+                            supplier = new TwoFiftyFiftyTargetAnimator();
+                        break;
+                        case SplitScreenConstants.SNAP_TO_2_33_66:
+                            break;
+                        case SplitScreenConstants.SNAP_TO_2_66_33:
+                            break;
+                        case SplitScreenConstants.SNAP_TO_END_AND_DISMISS:
+                            break;
+                        case SplitScreenConstants.SNAP_TO_MINIMIZE:
+                            break;
+                        case SplitScreenConstants.SNAP_TO_NONE:
+                            break;
+                        case SplitScreenConstants.SNAP_TO_START_AND_DISMISS:
+                            break;
+                        default:
+                    }
+
+                    Pair<List<Target>, List<List<HoverAnimProps>>> targetsAnims =
+                            supplier.getTargets(mSession.displayLayout,
+                                    insets, isLeftRightSplit, mContext.getResources());
+                    mTargets = new ArrayList<>(targetsAnims.getFirst());
+                    mHoverAnimProps.put(SNAP_TO_2_50_50, targetsAnims.getSecond());
+                    assert(mTargets.size() == targetsAnims.getSecond().size());
+                    if (DEBUG_LAYOUT) {
+                        for (List<HoverAnimProps> props : targetsAnims.getSecond()) {
+                            StringBuilder sb = new StringBuilder();
+                            for (HoverAnimProps hap : props) {
+                                sb.append(hap).append("\n");
+                            }
+                            sb.append("\n");
+                            Log.d(TAG, sb.toString());
+                        }
+                    }
                 }
-
-                mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, topOrLeftBounds));
-                mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, bottomOrRightBounds));
-
             } else {
-                final Rect topHitRegion = new Rect();
-                final Rect bottomHitRegion = new Rect();
+                // Already split, allow replacing existing split task
+                final Rect topOrLeftBounds = new Rect();
+                final Rect bottomOrRightBounds = new Rect();
+                mSplitScreen.getStageBounds(topOrLeftBounds, bottomOrRightBounds);
+                topOrLeftBounds.intersect(displayRegion);
+                bottomOrRightBounds.intersect(displayRegion);
 
-                // If we have existing split regions use those bounds, otherwise split it 50/50
-                if (inSplitScreen) {
-                    // The bounds of the existing split will have a divider bar, the hit region
-                    // should include that space. Find the center of the divider bar:
-                    float centerX = topOrLeftBounds.bottom + (dividerWidth / 2);
-                    // Now set the hit regions using that center.
-                    topHitRegion.set(displayRegion);
-                    topHitRegion.bottom = (int) centerX;
-                    bottomHitRegion.set(displayRegion);
-                    bottomHitRegion.top = (int) centerX;
+                if (isLeftRightSplit) {
+                    final Rect leftHitRegion = new Rect();
+                    final Rect rightHitRegion = new Rect();
+
+                    // If we have existing split regions use those bounds, otherwise split it 50/50
+                    if (inSplitScreen) {
+                        // The bounds of the existing split will have a divider bar, the hit region
+                        // should include that space. Find the center of the divider bar:
+                        float centerX = topOrLeftBounds.right + (dividerWidth / 2);
+                        // Now set the hit regions using that center.
+                        leftHitRegion.set(displayRegion);
+                        leftHitRegion.right = (int) centerX;
+                        rightHitRegion.set(displayRegion);
+                        rightHitRegion.left = (int) centerX;
+                    } else {
+                        displayRegion.splitVertically(leftHitRegion, rightHitRegion);
+                    }
+
+                    mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, topOrLeftBounds, -1));
+                    mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, bottomOrRightBounds,
+                            -1));
                 } else {
-                    displayRegion.splitHorizontally(topHitRegion, bottomHitRegion);
-                }
+                    final Rect topHitRegion = new Rect();
+                    final Rect bottomHitRegion = new Rect();
 
-                mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topOrLeftBounds));
-                mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomOrRightBounds));
+                    // If we have existing split regions use those bounds, otherwise split it 50/50
+                    if (inSplitScreen) {
+                        // The bounds of the existing split will have a divider bar, the hit region
+                        // should include that space. Find the center of the divider bar:
+                        float centerX = topOrLeftBounds.bottom + (dividerWidth / 2);
+                        // Now set the hit regions using that center.
+                        topHitRegion.set(displayRegion);
+                        topHitRegion.bottom = (int) centerX;
+                        bottomHitRegion.set(displayRegion);
+                        bottomHitRegion.top = (int) centerX;
+                    } else {
+                        displayRegion.splitHorizontally(topHitRegion, bottomHitRegion);
+                    }
+
+                    mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topOrLeftBounds, -1));
+                    mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomOrRightBounds,
+                            -1));
+                }
             }
         } else {
             // Split-screen not allowed, so only show the fullscreen target
-            mTargets.add(new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion));
+            mTargets.add(new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion, -1));
         }
         return mTargets;
     }
@@ -230,6 +330,22 @@
         }
         for (int i = mTargets.size() - 1; i >= 0; i--) {
             SplitDragPolicy.Target t = mTargets.get(i);
+            if (enableFlexibleSplit() && mCurrentHoverTarget != null) {
+                // If we're in flexible split, the targets themselves animate, so we have to rely
+                // on the view's animated position for subsequent drag coordinates which we also
+                // cache in HoverAnimProps.
+                List<List<HoverAnimProps>> hoverAnimPropTargets =
+                        mHoverAnimProps.get(mCurrentSnapPosition);
+                for (HoverAnimProps animProps :
+                        hoverAnimPropTargets.get(mCurrentHoverTarget.index)) {
+                    if (animProps.getHoverRect() != null &&
+                            animProps.getHoverRect().contains(x, y)) {
+                        return animProps.getTarget();
+                    }
+                }
+
+            }
+
             if (t.hitRegion.contains(x, y)) {
                 return t;
             }
@@ -266,6 +382,10 @@
         } else {
             launchIntent(mSession, starter, position, hideTaskToken);
         }
+
+        if (enableFlexibleSplit()) {
+            reset();
+        }
     }
 
     /**
@@ -335,6 +455,82 @@
                 null /* fillIntent */, position, opts, hideTaskToken);
     }
 
+    @Override
+    public void onHoveringOver(Target hoverTarget) {
+        final boolean isLeftRightSplit = mSplitScreen != null && mSplitScreen.isLeftRightSplit();
+        final boolean inSplitScreen = mSplitScreen != null && mSplitScreen.isSplitScreenVisible();
+        if (!inSplitScreen) {
+            // no need to animate for entering 50/50 split
+            return;
+        }
+
+        mCurrentHoverTarget = hoverTarget;
+        if (hoverTarget == null) {
+            // Reset to default state
+            BiConsumer<Target, View> biConsumer = new BiConsumer<Target, View>() {
+                @Override
+                public void accept(Target target, View view) {
+                    // take into account left/right split
+                    Animator transX = ObjectAnimator.ofFloat(view, View.TRANSLATION_X,
+                            target.drawRegion.left);
+                    Animator transY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y,
+                            target.drawRegion.top);
+                    Animator scaleX = ObjectAnimator.ofFloat(view, View.SCALE_X, 1);
+                    Animator scaleY = ObjectAnimator.ofFloat(view, View.SCALE_Y, 1);
+                    AnimatorSet as = new AnimatorSet();
+                    as.play(transX);
+                    as.play(transY);
+                    as.play(scaleX);
+                    as.play(scaleY);
+
+                    as.start();
+                }
+            };
+            mDragZoneAnimator.animateDragTargets(List.of(biConsumer));
+            return;
+        }
+
+        // TODO(b/349828130) get this from split controller
+        @SplitScreenConstants.SnapPosition int snapPosition = SNAP_TO_2_50_50;
+        mCurrentSnapPosition = SNAP_TO_2_50_50;
+        List<BiConsumer<Target, View>> animatingConsumers = new ArrayList<>();
+        final List<List<HoverAnimProps>> hoverAnimProps = mHoverAnimProps.get(snapPosition);
+        List<HoverAnimProps> animProps = hoverAnimProps.get(hoverTarget.index);
+        // Expand start and push out the rest to the end
+        BiConsumer<Target, View> biConsumer = new BiConsumer<>() {
+            @Override
+            public void accept(Target target, View view) {
+                if (animProps.isEmpty() || animProps.size() < (target.index + 1)) {
+                    return;
+                }
+                HoverAnimProps singleAnimProp = animProps.get(target.index);
+                Animator transX = ObjectAnimator.ofFloat(view, View.TRANSLATION_X,
+                        singleAnimProp.getTransX());
+                Animator transY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y,
+                        singleAnimProp.getTransY());
+                Animator scaleX = ObjectAnimator.ofFloat(view, View.SCALE_X,
+                        singleAnimProp.getScaleX());
+                Animator scaleY = ObjectAnimator.ofFloat(view, View.SCALE_Y,
+                        singleAnimProp.getScaleY());
+                AnimatorSet as = new AnimatorSet();
+                as.play(transX);
+                as.play(transY);
+                as.play(scaleX);
+                as.play(scaleY);
+                as.start();
+            }
+        };
+        animatingConsumers.add(biConsumer);
+        mDragZoneAnimator.animateDragTargets(animatingConsumers);
+    }
+
+    private void reset() {
+        mCurrentHoverTarget = null;
+        mCurrentSnapPosition = -1;
+    }
+
+
+
     /**
      * Interface for actually committing the task launches.
      */
@@ -425,7 +621,7 @@
      */
     public static class Target {
         static final int TYPE_FULLSCREEN = 0;
-        static final int TYPE_SPLIT_LEFT = 1;
+        public static final int TYPE_SPLIT_LEFT = 1;
         static final int TYPE_SPLIT_TOP = 2;
         static final int TYPE_SPLIT_RIGHT = 3;
         static final int TYPE_SPLIT_BOTTOM = 4;
@@ -445,16 +641,23 @@
         final Rect hitRegion;
         // The approximate visual region for where the task will start
         final Rect drawRegion;
+        int index;
 
-        public Target(@Type int t, Rect hit, Rect draw) {
+        /**
+         * @param index 0-indexed, represents which position of drop target this object represents,
+         *              0 to N for left to right, top to bottom
+         */
+        public Target(@Type int t, Rect hit, Rect draw, int index) {
             type = t;
             hitRegion = hit;
             drawRegion = draw;
+            this.index = index;
         }
 
         @Override
         public String toString() {
-            return "Target {type=" + type + " hit=" + hitRegion + " draw=" + drawRegion + "}";
+            return "Target {type=" + type + " hit=" + hitRegion + " draw=" + drawRegion
+                    + " index=" + index + "}";
         }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/DropTargetAnimSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/DropTargetAnimSupplier.kt
new file mode 100644
index 0000000..bb34613
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/DropTargetAnimSupplier.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.draganddrop.anim
+
+import android.content.res.Resources
+import android.graphics.Insets
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.draganddrop.SplitDragPolicy
+
+/**
+ * When the user is dragging an icon from Taskbar to add an app into split
+ * screen, we have a set of rules by which we draw and move colored drop
+ * targets around the screen. The rules are provided through this interface.
+ *
+ * Each possible screen layout should have an implementation of this interface.
+ * E.g.
+ * - 50:50 two-app split
+ * - 10:45:45 three-app split
+ * - single app, no split
+ *     = three implementations of this interface.
+ */
+interface DropTargetAnimSupplier {
+    /**
+     * Returns a Pair of lists.
+     * First list (length n): Where to draw the n colored drop zones.
+     * Second list (length n): How to animate the drop zones as user hovers around.
+     *
+     * Ex: First list => [A, B, C] // 3 views will be created representing these 3 targets
+     * Second list => [
+     *      [A (scaleX=4), B (translateX=20), C (translateX=20)], // hovering over A
+     *      [A (translateX=20), B (scaleX=4), C (translateX=20)], // hovering over B
+     *      [A (translateX=20), B (translateX=20), C (scaleX=4)], // hovering over C
+     *  ]
+     *
+     *  All indexes assume 0 to N => left to right when [isLeftRightSplit] is true and top to bottom
+     *  when [isLeftRightSplit] is false. Indexing is left to right even in RtL mode.
+     *
+     *  All lists should have the SAME number of elements, even if no animations are to be run for
+     *  a given target while in a hover state.
+     *  It's not that we don't trust you, but we _really_ don't trust you, so this will throw an
+     *  exception if lengths are different. Don't ruin it for everyone else...
+     *  or do. Idk, you're an adult.
+     */
+    fun getTargets(displayLayout: DisplayLayout, insets: Insets, isLeftRightSplit: Boolean,
+                   resources: Resources) :
+            Pair<List<SplitDragPolicy.Target>, List<List<HoverAnimProps>>>
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/HoverAnimProps.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/HoverAnimProps.kt
new file mode 100644
index 0000000..d61caeb
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/HoverAnimProps.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.draganddrop.anim
+
+import android.graphics.Rect
+import com.android.wm.shell.draganddrop.SplitDragPolicy
+
+/**
+ * Contains the animation props to represent a single state of drop targets.
+ * When the user is dragging, we'd be going between different HoverAnimProps
+ */
+data class HoverAnimProps(
+    var target: SplitDragPolicy.Target,
+    val transX: Float,
+    val transY: Float,
+    val scaleX: Float,
+    val scaleY: Float,
+    /**
+     * Pass in null to indicate this target cannot be hovered over for this given animation/
+     * state
+     *
+     * TODO: There's some way we can probably use the existing translation/scaling values
+     * to take [.target]'s hitRect and scale that so we don't have to take in a separate
+     * hoverRect in the CTOR. Have to make sure the pivots match since view's pivot in the
+     * center of the view and rect's pivot at 0, 0 if unspecified.
+     * The two may also not be correlated, but worth investigating
+     *
+     */
+    var hoverRect: Rect?
+) {
+
+    override fun toString(): String {
+        return ("targetId: " + target
+                + " translationX: " + transX
+                + " translationY: " + transY
+                + " scaleX: " + scaleX
+                + " scaleY: " + scaleY
+                + " hoverRect: " + hoverRect)
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt
new file mode 100644
index 0000000..9f532f5
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.draganddrop.anim
+
+import android.content.res.Resources
+import android.graphics.Insets
+import android.graphics.Rect
+import com.android.wm.shell.R
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.draganddrop.SplitDragPolicy.Target
+
+/**
+ * Represents Drop Zone targets and animations for when the system is currently in a 2 app 50/50
+ * split.
+ * SnapPosition = 2_50_50
+ *
+ * NOTE: Naming convention for many variables is done as "hXtYZ"
+ * This means that variable is a transformation on the Z property for target index Y while the user
+ * is hovering over target index X
+ * Ex: h1t2scaleX=2 => User is hovering over target index 1, target index 2 should scaleX by 2
+ *
+ * TODO(b/349828130): Everything in this class is temporary, none of this is up to spec.
+ */
+class TwoFiftyFiftyTargetAnimator : DropTargetAnimSupplier {
+    /**
+     * TODO: Could we transpose all the horizontal rects by 90 degrees and have that suffice for
+     *  top bottom split?? Hmmm... Doubt it.
+     */
+    override fun getTargets(
+        displayLayout: DisplayLayout,
+        insets: Insets,
+        isLeftRightSplit: Boolean,
+        resources: Resources
+    ): Pair<List<Target>, List<List<HoverAnimProps>>> {
+        val targets : ArrayList<Target> = ArrayList()
+        val w: Int = displayLayout.width()
+        val h: Int = displayLayout.height()
+        val iw = w - insets.left - insets.right
+        val ih = h - insets.top - insets.bottom
+        val l = insets.left
+        val t = insets.top
+        val displayRegion = Rect(l, t, l + iw, t + ih)
+        val fullscreenDrawRegion = Rect(displayRegion)
+        val dividerWidth: Float = resources.getDimensionPixelSize(
+            R.dimen.split_divider_bar_width
+        ).toFloat()
+
+        val farStartBounds = Rect()
+        farStartBounds.set(displayRegion)
+        val startBounds = Rect()
+        startBounds.set(displayRegion)
+        val endBounds = Rect()
+        endBounds.set(displayRegion)
+        val farEndBounds = Rect()
+        farEndBounds.set(displayRegion)
+        val endsPercent = 0.10f
+        val visibleStagePercent = 0.45f
+        val halfDividerWidth = dividerWidth.toInt() / 2
+        val endsWidth = Math.round(displayRegion.width() * endsPercent)
+        val stageWidth = Math.round(displayRegion.width() * visibleStagePercent)
+
+
+        // Place the farStart and farEnds outside of the display, and then
+        // animate them in once the hover starts
+        // | = divider; || = display boundary
+        // farStart || start | end || farEnd
+        farStartBounds.left = -endsWidth
+        farStartBounds.right = 0
+        startBounds.left = farStartBounds.right + dividerWidth.toInt()
+        startBounds.right = startBounds.left + stageWidth
+        endBounds.left = startBounds.right + dividerWidth.toInt()
+        endBounds.right = endBounds.left + stageWidth
+        farEndBounds.left = fullscreenDrawRegion.right
+        farEndBounds.right = farEndBounds.left + endsWidth
+
+
+        // For the hit rect, trim the divider space we've added between the
+        // rects
+        targets.add(
+            Target(
+                Target.TYPE_SPLIT_LEFT,
+                Rect(
+                    farStartBounds.left, farStartBounds.top,
+                    farStartBounds.right + halfDividerWidth,
+                    farStartBounds.bottom
+                ),
+                farStartBounds, 0
+            )
+        )
+        targets.add(
+            Target(
+                Target.TYPE_SPLIT_LEFT,
+                Rect(
+                    startBounds.left - halfDividerWidth,
+                    startBounds.top,
+                    startBounds.right + halfDividerWidth,
+                    startBounds.bottom
+                ),
+                startBounds, 1
+            )
+        )
+        targets.add(
+            Target(
+                Target.TYPE_SPLIT_LEFT,
+                Rect(
+                    endBounds.left - halfDividerWidth,
+                    endBounds.top, endBounds.right, endBounds.bottom
+                ),
+                endBounds, 2
+            )
+        )
+        targets.add(
+            Target(
+                Target.TYPE_SPLIT_LEFT,
+                Rect(
+                    farEndBounds.left - halfDividerWidth,
+                    farEndBounds.top, farEndBounds.right, farEndBounds.bottom
+                ),
+                farEndBounds, 3
+            )
+        )
+
+
+        // Hovering over target 0,
+        // * increase scaleX of target 0
+        // * decrease scaleX of target 1, 2
+        // * ensure target 3 offscreen
+
+        // bring target 0 in from offscreen and expand
+        val h0t0ScaleX = stageWidth.toFloat() / endsWidth
+        val h0t0TransX: Float = stageWidth / h0t0ScaleX + dividerWidth
+        val h0t0HoverProps = HoverAnimProps(
+            targets.get(0),
+            h0t0TransX, farStartBounds.top.toFloat(), h0t0ScaleX, 1f,
+            Rect(
+                0, 0, (stageWidth + dividerWidth).toInt(),
+                farStartBounds.bottom
+            )
+        )
+
+
+        // move target 1 over to the middle/end
+        val h0t1TransX = stageWidth.toFloat()
+        val h0t1ScaleX = 1f
+        val h0t1HoverProps = HoverAnimProps(
+            targets.get(1),
+            h0t1TransX, startBounds.top.toFloat(), h0t1ScaleX, 1f,
+            Rect(
+                stageWidth, 0, (stageWidth + h0t1TransX).toInt(),
+                farStartBounds.bottom
+            )
+        )
+
+
+        // move target 2 to the very end
+        val h0t2TransX = endBounds.left + stageWidth / 2f
+        val h0t2ScaleX = endsWidth.toFloat() / stageWidth
+        val h0t2HoverProps = HoverAnimProps(
+            targets.get(2),
+            h0t2TransX, endBounds.top.toFloat(), h0t2ScaleX, 1f,
+            Rect(
+                displayRegion.right as Int - endsWidth, 0,
+                displayRegion.right as Int,
+                farStartBounds.bottom
+            )
+        )
+
+
+        // move target 3 off-screen
+        val h0t3TransX = farEndBounds.right.toFloat()
+        val h0t3ScaleX = 1f
+        val h0t3HoverProps = HoverAnimProps(
+            targets.get(3),
+            h0t3TransX, farEndBounds.top.toFloat(), h0t3ScaleX, 1f,
+            null
+        )
+        val animPropsForHoverTarget0 =
+            listOf(h0t0HoverProps, h0t1HoverProps, h0t2HoverProps, h0t3HoverProps)
+
+
+        // Hovering over target 1,
+        // * Bring in target 0 from offscreen start
+        // * Shift over target 1
+        // * Slightly lower scale of target 2
+        // * Ensure target 4 offscreen
+        // bring target 0 in from offscreen
+        val h1t0TransX = 0f
+        val h1t0ScaleX = 1f
+        val h1t0HoverProps = HoverAnimProps(
+            targets.get(0),
+            h1t0TransX, farStartBounds.top.toFloat(), h1t0ScaleX, 1f,
+            Rect(
+                0, 0, (farStartBounds.width() + dividerWidth).toInt(),
+                farStartBounds.bottom
+            )
+        )
+
+
+        // move target 1 over a tiny bit by same amount and make it smaller
+        val h1t1TransX: Float = endsWidth + dividerWidth
+        val h1t1ScaleX = 1f
+        val h1t1HoverProps = HoverAnimProps(
+            targets.get(1),
+            h1t1TransX, startBounds.top.toFloat(), h1t1ScaleX, 1f,
+            Rect(
+                h1t1TransX.toInt(), 0, (h1t1TransX + stageWidth).toInt(),
+                farStartBounds.bottom
+            )
+        )
+
+
+        // move target 2 to the very end
+        val h1t2TransX = (endBounds.left + farStartBounds.width()).toFloat()
+        val h1t2ScaleX = h1t1ScaleX
+        val h1t2HoverProps = HoverAnimProps(
+            targets.get(2),
+            h1t2TransX, endBounds.top.toFloat(), h1t2ScaleX, 1f,
+            Rect(
+                endBounds.left + farStartBounds.width(),
+                0,
+                (endBounds.left + farStartBounds.width() + stageWidth),
+                farStartBounds.bottom
+            )
+        )
+
+
+        // move target 3 off-screen, default laid out is off-screen
+        val h1t3TransX = farEndBounds.right.toFloat()
+        val h1t3ScaleX = 1f
+        val h1t3HoverProps = HoverAnimProps(
+            targets.get(3),
+            h1t3TransX, farEndBounds.top.toFloat(), h1t3ScaleX, 1f,
+            null
+        )
+        val animPropsForHoverTarget1 =
+            listOf(h1t0HoverProps, h1t1HoverProps, h1t2HoverProps, h1t3HoverProps)
+
+
+        // Hovering over target 2,
+        // * Ensure Target 0 offscreen
+        // * Ensure target 1 back to start, slightly smaller scale
+        // * Slightly lower scale of target 2
+        // * Bring target 4 on screen
+        // reset target 0
+        val h2t0TransX = farStartBounds.left.toFloat()
+        val h2t0ScaleX = 1f
+        val h2t0HoverProps = HoverAnimProps(
+            targets.get(0),
+            h2t0TransX, farStartBounds.top.toFloat(), h2t0ScaleX, 1f,
+            null
+        )
+
+
+        // move target 1 over a tiny bit by same amount and make it smaller
+        val h2t1TransX = startBounds.left.toFloat()
+        val h2t1ScaleX = 1f
+        val h2t1HoverProps = HoverAnimProps(
+            targets.get(1),
+            h2t1TransX, startBounds.top.toFloat(), h2t1ScaleX, 1f,
+            Rect(
+                startBounds.left, 0,
+                (startBounds.left + stageWidth),
+                farStartBounds.bottom
+            )
+        )
+
+
+        // move target 2 to the very end
+        val h2t2TransX = endBounds.left.toFloat()
+        val h2t2ScaleX = h2t1ScaleX
+        val h2t2HoverProps = HoverAnimProps(
+            targets.get(2),
+            h2t2TransX, endBounds.top.toFloat(), h2t2ScaleX, 1f,
+            Rect(
+                (startBounds.right + dividerWidth).toInt(),
+                0,
+                endBounds.left + stageWidth,
+                farStartBounds.bottom
+            )
+        )
+
+
+        // bring target 3 on-screen
+        val h2t3TransX = (farEndBounds.left - farEndBounds.width()).toFloat()
+        val h2t3ScaleX = 1f
+        val h2t3HoverProps = HoverAnimProps(
+            targets.get(3),
+            h2t3TransX, farEndBounds.top.toFloat(), h2t3ScaleX, 1f,
+            Rect(
+                endBounds.right,
+                0,
+                displayRegion.right,
+                farStartBounds.bottom
+            )
+        )
+        val animPropsForHoverTarget2 =
+            listOf(h2t0HoverProps, h2t1HoverProps, h2t2HoverProps, h2t3HoverProps)
+
+
+        // Hovering over target 3,
+        // * Ensure Target 0 offscreen
+        // * Ensure target 1 back to start, slightly smaller scale
+        // * Slightly lower scale of target 2
+        // * Bring target 4 on screen and scale up
+        // reset target 0
+        val h3t0TransX = farStartBounds.left.toFloat()
+        val h3t0ScaleX = 1f
+        val h3t0HoverProps = HoverAnimProps(
+            targets.get(0),
+            h3t0TransX, farStartBounds.top.toFloat(), h3t0ScaleX, 1f,
+            null
+        )
+
+
+        // move target 1 over a tiny bit by same amount and make it smaller
+        val h3t1ScaleX = endsWidth.toFloat() / stageWidth
+        val h3t1TransX = 0 - (stageWidth / (1 / h3t1ScaleX))
+        val h3t1HoverProps = HoverAnimProps(
+            targets.get(1),
+            h3t1TransX, startBounds.top.toFloat(), h3t1ScaleX, 1f,
+            Rect(
+                0, 0,
+                endsWidth,
+                farStartBounds.bottom
+            )
+        )
+
+
+        // move target 2 towards the start
+        val h3t2TransX: Float = endsWidth + dividerWidth
+        val h3t2ScaleX = 1f
+        val h3t2HoverProps = HoverAnimProps(
+            targets.get(2),
+            h3t2TransX, endBounds.top.toFloat(), h3t2ScaleX, 1f,
+            Rect(
+                endsWidth, 0,
+                (endsWidth + stageWidth + dividerWidth).toInt(),
+                farStartBounds.bottom
+            )
+        )
+
+
+        // bring target 3 on-screen and expand
+        val h3t3ScaleX = stageWidth.toFloat() / endsWidth
+        val h3t3TransX = endBounds.right - stageWidth / 2f
+        val h3t3HoverProps = HoverAnimProps(
+            targets.get(3),
+            h3t3TransX, farEndBounds.top.toFloat(), h3t3ScaleX, 1f,
+            Rect(
+                displayRegion.right - stageWidth, 0,
+                displayRegion.right,
+                farStartBounds.bottom
+            )
+        )
+        val animPropsForHoverTarget3 =
+            listOf(h3t0HoverProps, h3t1HoverProps, h3t2HoverProps, h3t3HoverProps)
+
+        return Pair(targets, listOf(animPropsForHoverTarget0, animPropsForHoverTarget1,
+            animPropsForHoverTarget2, animPropsForHoverTarget3))
+
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 87b661d..e77467d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -322,6 +322,22 @@
         return mTaskOrganizer.getRunningTaskInfo(taskId);
     }
 
+    /**
+     * @return an Array of RunningTaskInfo's ordered by leftToRight or topTopBottom
+     */
+    @Nullable
+    public ActivityManager.RunningTaskInfo[] getAllTaskInfos() {
+        // TODO(b/349828130) Add the third stage task info and not rely on positions
+        ActivityManager.RunningTaskInfo topLeftTask = getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+        ActivityManager.RunningTaskInfo bottomRightTask =
+                getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+        if (topLeftTask != null && bottomRightTask != null) {
+            return new ActivityManager.RunningTaskInfo[]{topLeftTask, bottomRightTask};
+        }
+
+        return null;
+    }
+
     /** Check task is under split or not by taskId. */
     public boolean isTaskInSplitScreen(int taskId) {
         return mStageCoordinator.getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java
index 46b60499..eb74218 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java
@@ -150,7 +150,8 @@
         mPortraitDisplayLayout = new DisplayLayout(info2, res, false, false);
         mInsets = Insets.of(0, 0, 0, 0);
 
-        mPolicy = spy(new SplitDragPolicy(mContext, mSplitScreenStarter, mFullscreenStarter));
+        mPolicy = spy(new SplitDragPolicy(mContext, mSplitScreenStarter, mFullscreenStarter,
+                mock(DragZoneAnimator.class)));
         mActivityClipData = createAppClipData(MIMETYPE_APPLICATION_ACTIVITY);
         mLaunchableIntentPendingIntent = mock(PendingIntent.class);
         when(mLaunchableIntentPendingIntent.getCreatorUserHandle())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ZenModesCleanupStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ZenModesCleanupStartableTest.kt
new file mode 100644
index 0000000..4a53a7a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ZenModesCleanupStartableTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy
+
+import android.app.AutomaticZenRule
+import android.app.NotificationManager
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ZenModesCleanupStartableTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    @Mock private lateinit var notificationManager: NotificationManager
+
+    private lateinit var underTest: ZenModesCleanupStartable
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            ZenModesCleanupStartable(
+                testScope.backgroundScope,
+                kosmos.backgroundCoroutineContext,
+                notificationManager,
+            )
+    }
+
+    @Test
+    fun start_withGamingModeZenRule_deletesIt() =
+        testScope.runTest {
+            whenever(notificationManager.automaticZenRules)
+                .thenReturn(
+                    mutableMapOf(
+                        Pair(
+                            "gaming",
+                            AutomaticZenRule.Builder(
+                                    "Gaming Mode",
+                                    Uri.parse(
+                                        "android-app://com.android.systemui/game-mode-dnd-controller"
+                                    ),
+                                )
+                                .setPackage("com.android.systemui")
+                                .build(),
+                        ),
+                        Pair(
+                            "other",
+                            AutomaticZenRule.Builder("Other Mode", Uri.parse("something-else"))
+                                .setPackage("com.other.package")
+                                .build(),
+                        ),
+                    )
+                )
+
+            underTest.start()
+            runCurrent()
+
+            verify(notificationManager).removeAutomaticZenRule(eq("gaming"))
+        }
+
+    @Test
+    fun start_withoutGamingModeZenRule_doesNothing() =
+        testScope.runTest {
+            whenever(notificationManager.automaticZenRules)
+                .thenReturn(
+                    mutableMapOf(
+                        Pair(
+                            "other",
+                            AutomaticZenRule.Builder("Other Mode", Uri.parse("something-else"))
+                                .setPackage("com.android.systemui")
+                                .build(),
+                        )
+                    )
+                )
+
+            underTest.start()
+            runCurrent()
+
+            verify(notificationManager, never()).removeAutomaticZenRule(any())
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index ca5f49d..684ce48 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -88,6 +88,7 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarter;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.ZenModesCleanupStartable;
 
 import dagger.Binds;
 import dagger.Module;
@@ -299,4 +300,10 @@
             ZenModeRepository repository) {
         return new NotificationsSoundPolicyInteractor(repository);
     }
+
+    /** Binds {@link ZenModesCleanupStartable} as a {@link CoreStartable}. */
+    @Binds
+    @IntoMap
+    @ClassKey(ZenModesCleanupStartable.class)
+    CoreStartable bindsZenModesCleanup(ZenModesCleanupStartable zenModesCleanup);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModesCleanupStartable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModesCleanupStartable.kt
new file mode 100644
index 0000000..32b476b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModesCleanupStartable.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy
+
+import android.app.NotificationManager
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.modes.shared.ModesUi
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Cleanup task that deletes the obsolete "Gaming" AutomaticZenRule that was created by SystemUI in
+ * the faraway past, and still exists on some devices through upgrades or B&R.
+ */
+// TODO: b/372874878 - Remove this thing once it has run long enough
+class ZenModesCleanupStartable
+@Inject
+constructor(
+    @Application private val applicationCoroutineScope: CoroutineScope,
+    @Background private val bgContext: CoroutineContext,
+    val notificationManager: NotificationManager,
+) : CoreStartable {
+
+    override fun start() {
+        if (!ModesUi.isEnabled) {
+            return
+        }
+        applicationCoroutineScope.launch { deleteObsoleteGamingMode() }
+    }
+
+    private suspend fun deleteObsoleteGamingMode() {
+        withContext(bgContext) {
+            val allRules = notificationManager.automaticZenRules
+            val gamingModeEntry =
+                allRules.entries.firstOrNull { entry ->
+                    entry.value.packageName == "com.android.systemui" &&
+                        entry.value.conditionId?.toString() ==
+                            "android-app://com.android.systemui/game-mode-dnd-controller"
+                }
+            if (gamingModeEntry != null) {
+                notificationManager.removeAutomaticZenRule(gamingModeEntry.key)
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
index 5514ec7..dc2c957 100644
--- a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
+++ b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
@@ -62,7 +62,6 @@
 import com.android.internal.pm.parsing.PackageParser2;
 import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
 import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.LocalServices;
 import com.android.server.integrity.engine.RuleEvaluationEngine;
 import com.android.server.integrity.model.IntegrityCheckResult;
@@ -214,12 +213,6 @@
                                         version, ruleProvider));
                     }
 
-                    FrameworkStatsLog.write(
-                            FrameworkStatsLog.INTEGRITY_RULES_PUSHED,
-                            success,
-                            ruleProvider,
-                            version);
-
                     Intent intent = new Intent();
                     intent.putExtra(EXTRA_STATUS, success ? STATUS_SUCCESS : STATUS_FAILURE);
                     try {
@@ -346,15 +339,6 @@
                                 packageName, result.getEffect(), result.getMatchedRules()));
             }
 
-            FrameworkStatsLog.write(
-                    FrameworkStatsLog.INTEGRITY_CHECK_RESULT_REPORTED,
-                    packageName,
-                    appCertificates.toString(),
-                    appInstallMetadata.getVersionCode(),
-                    installerPackageName,
-                    result.getLoggingResponse(),
-                    result.isCausedByAppCertRule(),
-                    result.isCausedByInstallerRule());
             mPackageManagerInternal.setIntegrityVerificationResult(
                     verificationId,
                     result.getEffect() == IntegrityCheckResult.Effect.ALLOW
diff --git a/services/core/java/com/android/server/integrity/model/IntegrityCheckResult.java b/services/core/java/com/android/server/integrity/model/IntegrityCheckResult.java
index 1fa0670..b0647fc 100644
--- a/services/core/java/com/android/server/integrity/model/IntegrityCheckResult.java
+++ b/services/core/java/com/android/server/integrity/model/IntegrityCheckResult.java
@@ -19,8 +19,6 @@
 import android.annotation.Nullable;
 import android.content.integrity.Rule;
 
-import com.android.internal.util.FrameworkStatsLog;
-
 import java.util.Collections;
 import java.util.List;
 
@@ -82,21 +80,6 @@
         return new IntegrityCheckResult(Effect.DENY, ruleList);
     }
 
-    /**
-     * Returns the in value of the integrity check result for logging purposes.
-     */
-    public int getLoggingResponse() {
-        if (getEffect() == Effect.DENY) {
-            return FrameworkStatsLog.INTEGRITY_CHECK_RESULT_REPORTED__RESPONSE__REJECTED;
-        } else if (getEffect() == Effect.ALLOW && getMatchedRules().isEmpty()) {
-            return FrameworkStatsLog.INTEGRITY_CHECK_RESULT_REPORTED__RESPONSE__ALLOWED;
-        } else if (getEffect() == Effect.ALLOW && !getMatchedRules().isEmpty()) {
-            return FrameworkStatsLog.INTEGRITY_CHECK_RESULT_REPORTED__RESPONSE__FORCE_ALLOWED;
-        } else {
-            throw new IllegalStateException("IntegrityCheckResult is not valid.");
-        }
-    }
-
     /** Returns true when the {@code mEffect} is caused by an app certificate mismatch. */
     public boolean isCausedByAppCertRule() {
         return mRuleList.stream().anyMatch(rule -> rule.getFormula().isAppCertificateFormula());
diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
index 006a5bb..a235ba15 100644
--- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
+++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
@@ -148,8 +148,8 @@
             case HapticFeedbackConstants.SCROLL_TICK:
             case HapticFeedbackConstants.SCROLL_ITEM_FOCUS:
             case HapticFeedbackConstants.SCROLL_LIMIT:
-                attrs = hapticFeedbackInputSourceCustomizationEnabled() ? TOUCH_VIBRATION_ATTRIBUTES
-                        : HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES;
+                // TODO(b/372820923): use touch attributes by default.
+                attrs = HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES;
                 break;
             case HapticFeedbackConstants.KEYBOARD_TAP:
             case HapticFeedbackConstants.KEYBOARD_RELEASE:
@@ -176,14 +176,15 @@
             int inputSource,
             @HapticFeedbackConstants.Flags int flags,
             @HapticFeedbackConstants.PrivateFlags int privFlags) {
-        if (hapticFeedbackInputSourceCustomizationEnabled()
-                && inputSource == InputDevice.SOURCE_ROTARY_ENCODER) {
+        if (hapticFeedbackInputSourceCustomizationEnabled()) {
             switch (effectId) {
                 case HapticFeedbackConstants.SCROLL_TICK,
                         HapticFeedbackConstants.SCROLL_ITEM_FOCUS,
                         HapticFeedbackConstants.SCROLL_LIMIT -> {
-                    return getVibrationAttributesWithFlags(HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES,
-                            effectId, flags);
+                    VibrationAttributes attrs = inputSource == InputDevice.SOURCE_ROTARY_ENCODER
+                            ? HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES
+                            : TOUCH_VIBRATION_ATTRIBUTES;
+                    return getVibrationAttributesWithFlags(attrs, effectId, flags);
                 }
             }
         }
diff --git a/services/tests/servicestests/src/com/android/server/integrity/model/IntegrityCheckResultTest.java b/services/tests/servicestests/src/com/android/server/integrity/model/IntegrityCheckResultTest.java
index 6c23ff6..d31ed68 100644
--- a/services/tests/servicestests/src/com/android/server/integrity/model/IntegrityCheckResultTest.java
+++ b/services/tests/servicestests/src/com/android/server/integrity/model/IntegrityCheckResultTest.java
@@ -22,8 +22,6 @@
 import android.content.integrity.CompoundFormula;
 import android.content.integrity.Rule;
 
-import com.android.internal.util.FrameworkStatsLog;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -40,8 +38,6 @@
 
         assertThat(allowResult.getEffect()).isEqualTo(IntegrityCheckResult.Effect.ALLOW);
         assertThat(allowResult.getMatchedRules()).isEmpty();
-        assertThat(allowResult.getLoggingResponse())
-                .isEqualTo(FrameworkStatsLog.INTEGRITY_CHECK_RESULT_REPORTED__RESPONSE__ALLOWED);
     }
 
     @Test
@@ -58,9 +54,6 @@
 
         assertThat(allowResult.getEffect()).isEqualTo(IntegrityCheckResult.Effect.ALLOW);
         assertThat(allowResult.getMatchedRules()).containsExactly(forceAllowRule);
-        assertThat(allowResult.getLoggingResponse())
-                .isEqualTo(
-                        FrameworkStatsLog.INTEGRITY_CHECK_RESULT_REPORTED__RESPONSE__FORCE_ALLOWED);
     }
 
     @Test
@@ -77,8 +70,6 @@
 
         assertThat(denyResult.getEffect()).isEqualTo(IntegrityCheckResult.Effect.DENY);
         assertThat(denyResult.getMatchedRules()).containsExactly(failedRule);
-        assertThat(denyResult.getLoggingResponse())
-                .isEqualTo(FrameworkStatsLog.INTEGRITY_CHECK_RESULT_REPORTED__RESPONSE__REJECTED);
     }
 
     @Test
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
index f7127df..3b2f532 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
@@ -453,20 +453,7 @@
     }
 
     @Test
-    public void testVibrationAttribute_scrollFeedback_inputCustomizedFlag_useTouchUsage() {
-        mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED);
-        HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations();
-
-        for (int effectId : SCROLL_FEEDBACK_CONSTANTS) {
-            VibrationAttributes attrs = provider.getVibrationAttributes(effectId, /* flags */
-                    0, /* privFlags */ 0);
-            assertWithMessage("Expected USAGE_TOUCH for scroll effect " + effectId
-                    + ", if no input customization").that(attrs.getUsage()).isEqualTo(USAGE_TOUCH);
-        }
-    }
-
-    @Test
-    public void testVibrationAttribute_scrollFeedback_noInputCustomizedFlag_useHardwareFeedback() {
+    public void testVibrationAttribute_scrollFeedback_useHardwareFeedback() {
         HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations();
 
         for (int effectId : SCROLL_FEEDBACK_CONSTANTS) {