Merge "Ignore public volume state change event when user is null" into sc-dev
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
index 7ca77d4..3f6e8a5 100644
--- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
+++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
@@ -16,6 +16,7 @@
 package com.android.server.appsearch;
 
 import static android.app.appsearch.AppSearchResult.throwableToFailedResult;
+import static android.os.Process.INVALID_UID;
 import static android.os.UserHandle.USER_NULL;
 
 import android.annotation.ElapsedRealtimeLong;
@@ -53,7 +54,6 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
-import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.LocalServices;
@@ -128,6 +128,17 @@
         mContext.registerReceiverAsUser(new UserActionReceiver(), UserHandle.ALL,
                 new IntentFilter(Intent.ACTION_USER_REMOVED), /*broadcastPermission=*/ null,
                 /*scheduler=*/ null);
+
+        //TODO(b/145759910) Add a direct callback when user clears the data instead of relying on
+        // broadcasts
+        IntentFilter packageChangedFilter = new IntentFilter();
+        packageChangedFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
+        packageChangedFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
+        packageChangedFilter.addDataScheme("package");
+        packageChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+        mContext.registerReceiverAsUser(new PackageChangedReceiver(), UserHandle.ALL,
+                packageChangedFilter, /*broadcastPermission=*/ null,
+                /*scheduler=*/ null);
     }
 
     private class UserActionReceiver extends BroadcastReceiver {
@@ -135,15 +146,15 @@
         public void onReceive(@NonNull Context context, @NonNull Intent intent) {
             switch (intent.getAction()) {
                 case Intent.ACTION_USER_REMOVED:
-                    final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
+                    int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
                     if (userId == USER_NULL) {
-                        Slog.e(TAG, "userId is missing in the intent: " + intent);
+                        Log.e(TAG, "userId is missing in the intent: " + intent);
                         return;
                     }
                     handleUserRemoved(userId);
                     break;
                 default:
-                    Slog.e(TAG, "Received unknown intent: " + intent);
+                    Log.e(TAG, "Received unknown intent: " + intent);
             }
         }
     }
@@ -163,9 +174,52 @@
         try {
             mImplInstanceManager.removeAppSearchImplForUser(userId);
             mLoggerInstanceManager.removePlatformLoggerForUser(userId);
-            Slog.i(TAG, "Removed AppSearchImpl instance for user: " + userId);
+            Log.i(TAG, "Removed AppSearchImpl instance for user: " + userId);
         } catch (Throwable t) {
-            Slog.e(TAG, "Unable to remove data for user: " + userId, t);
+            Log.e(TAG, "Unable to remove data for user: " + userId, t);
+        }
+    }
+
+    private class PackageChangedReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+            switch (intent.getAction()) {
+                case Intent.ACTION_PACKAGE_FULLY_REMOVED:
+                case Intent.ACTION_PACKAGE_DATA_CLEARED:
+                    String packageName = intent.getData().getSchemeSpecificPart();
+                    if (packageName == null) {
+                        Log.e(TAG, "Package name is missing in the intent: " + intent);
+                        return;
+                    }
+                    int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
+                    if (uid == INVALID_UID) {
+                        Log.e(TAG, "uid is missing in the intent: " + intent);
+                        return;
+                    }
+                    handlePackageRemoved(packageName, uid);
+                    break;
+                default:
+                    Log.e(TAG, "Received unknown intent: " + intent);
+            }
+        }
+    }
+
+    private void handlePackageRemoved(String packageName, int uid) {
+        int userId = UserHandle.getUserId(uid);
+        try {
+            if (isUserLocked(userId)) {
+                //TODO(b/186151459) clear the uninstalled package data when user is unlocked.
+                return;
+            }
+            if (ImplInstanceManager.getAppSearchDir(userId).exists()) {
+                // Only clear the package's data if AppSearch exists for this user.
+                AppSearchImpl impl = mImplInstanceManager.getOrCreateAppSearchImpl(mContext,
+                        userId);
+                //TODO(b/145759910) clear visibility setting for package.
+                impl.clearPackageData(packageName);
+            }
+        } catch (Throwable t) {
+            Log.e(TAG, "Unable to remove data for package: " + packageName, t);
         }
     }
 
@@ -177,17 +231,20 @@
     }
 
     private void verifyUserUnlocked(int callingUserId) {
+        if (isUserLocked(callingUserId)) {
+            throw new IllegalStateException("User " + callingUserId + " is locked or not running.");
+        }
+    }
+
+    private boolean isUserLocked(int callingUserId) {
         synchronized (mUnlockedUserIdsLocked) {
             // First, check the local copy.
             if (mUnlockedUserIdsLocked.contains(callingUserId)) {
-                return;
+                return false;
             }
             // If the local copy says the user is locked, check with UM for the actual state,
             // since the user might just have been unlocked.
-            if (!mUserManager.isUserUnlockingOrUnlocked(UserHandle.of(callingUserId))) {
-                throw new IllegalStateException(
-                        "User " + callingUserId + " is locked or not running.");
-            }
+            return !mUserManager.isUserUnlockingOrUnlocked(UserHandle.of(callingUserId));
         }
     }
 
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/ImplInstanceManager.java b/apex/appsearch/service/java/com/android/server/appsearch/ImplInstanceManager.java
index af39b79..45023f9 100644
--- a/apex/appsearch/service/java/com/android/server/appsearch/ImplInstanceManager.java
+++ b/apex/appsearch/service/java/com/android/server/appsearch/ImplInstanceManager.java
@@ -73,6 +73,15 @@
     }
 
     /**
+     * Returns AppSearch directory in the credential encrypted system directory for the given user.
+     *
+     * <p>This folder should only be accessed after unlock.
+     */
+    public static File getAppSearchDir(@UserIdInt int userId) {
+        return new File(Environment.getDataSystemCeDirectory(userId), APP_SEARCH_DIR);
+    }
+
+    /**
      * Gets an instance of AppSearchImpl for the given user, or creates one if none exists.
      *
      * <p>If no AppSearchImpl instance exists for the unlocked user, Icing will be initialized and
@@ -139,15 +148,10 @@
 
     private AppSearchImpl createImpl(@NonNull Context context, @UserIdInt int userId)
             throws AppSearchException {
-        File appSearchDir = getAppSearchDir(context, userId);
+        File appSearchDir = getAppSearchDir(userId);
         return AppSearchImpl.create(appSearchDir, context, userId, mGlobalQuerierPackage);
     }
 
-    private static File getAppSearchDir(@NonNull Context context, @UserIdInt int userId) {
-        // See com.android.internal.app.ChooserActivity::getPinnedSharedPrefs
-        return new File(Environment.getDataSystemCeDirectory(userId), APP_SEARCH_DIR);
-    }
-
     /**
      * Returns the global querier package if it's a system package. Otherwise, empty string.
      *
diff --git a/core/api/current.txt b/core/api/current.txt
index c3c7919..4cdc519 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -56795,6 +56795,7 @@
   public interface SplashScreen {
     method public void clearOnExitAnimationListener();
     method public void setOnExitAnimationListener(@NonNull android.window.SplashScreen.OnExitAnimationListener);
+    method public void setSplashScreenTheme(@StyleRes int);
   }
 
   public static interface SplashScreen.OnExitAnimationListener {
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 7fe2a41..5e72325 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -796,6 +796,10 @@
 
     void setMimeGroup(String packageName, String group, in List<String> mimeTypes);
 
+    String getSplashScreenTheme(String packageName, int userId);
+
+    void setSplashScreenTheme(String packageName, String themeName, int userId);
+
     List<String> getMimeGroup(String packageName, String group);
 
     boolean isAutoRevokeWhitelisted(String packageName);
diff --git a/core/java/android/content/pm/PackageUserState.java b/core/java/android/content/pm/PackageUserState.java
index 55a6ab7..84317b3 100644
--- a/core/java/android/content/pm/PackageUserState.java
+++ b/core/java/android/content/pm/PackageUserState.java
@@ -81,6 +81,7 @@
     public int installReason;
     public @PackageManager.UninstallReason int uninstallReason;
     public String harmfulAppWarning;
+    public String splashScreenTheme;
 
     public ArraySet<String> disabledComponents;
     public ArraySet<String> enabledComponents;
@@ -130,6 +131,7 @@
         if (o.componentLabelIconOverrideMap != null) {
             this.componentLabelIconOverrideMap = new ArrayMap<>(o.componentLabelIconOverrideMap);
         }
+        splashScreenTheme = o.splashScreenTheme;
     }
 
     @Nullable
@@ -242,6 +244,7 @@
         return componentLabelIconOverrideMap.get(componentName);
     }
 
+
     /**
      * Test if this package is installed.
      */
@@ -479,7 +482,11 @@
         }
         if (harmfulAppWarning == null && oldState.harmfulAppWarning != null
                 || (harmfulAppWarning != null
-                        && !harmfulAppWarning.equals(oldState.harmfulAppWarning))) {
+                && !harmfulAppWarning.equals(oldState.harmfulAppWarning))) {
+            return false;
+        }
+
+        if (!Objects.equals(splashScreenTheme, oldState.splashScreenTheme)) {
             return false;
         }
         return true;
@@ -505,6 +512,7 @@
         hashCode = 31 * hashCode + Objects.hashCode(disabledComponents);
         hashCode = 31 * hashCode + Objects.hashCode(enabledComponents);
         hashCode = 31 * hashCode + Objects.hashCode(harmfulAppWarning);
+        hashCode = 31 * hashCode + Objects.hashCode(splashScreenTheme);
         return hashCode;
     }
 
diff --git a/core/java/android/hardware/face/OWNERS b/core/java/android/hardware/face/OWNERS
index be10df1..0b4d9d9 100644
--- a/core/java/android/hardware/face/OWNERS
+++ b/core/java/android/hardware/face/OWNERS
@@ -1,7 +1,3 @@
 # Bug component: 879035
 
-curtislb@google.com
-ilyamaty@google.com
-jaggies@google.com
-joshmccloskey@google.com
-kchyn@google.com
+include /services/core/java/com/android/server/biometrics/OWNERS
diff --git a/core/java/android/hardware/fingerprint/OWNERS b/core/java/android/hardware/fingerprint/OWNERS
index e55b8c56..5c93672 100644
--- a/core/java/android/hardware/fingerprint/OWNERS
+++ b/core/java/android/hardware/fingerprint/OWNERS
@@ -1,8 +1,3 @@
 # Bug component: 114777
 
-curtislb@google.com
-ilyamaty@google.com
-jaggies@google.com
-joshmccloskey@google.com
-kchyn@google.com
-
+include /services/core/java/com/android/server/biometrics/OWNERS
diff --git a/core/java/android/hardware/iris/OWNERS b/core/java/android/hardware/iris/OWNERS
index 33527f8..0b4d9d9 100644
--- a/core/java/android/hardware/iris/OWNERS
+++ b/core/java/android/hardware/iris/OWNERS
@@ -1,3 +1,3 @@
 # Bug component: 879035
 
-jaggies@google.com
+include /services/core/java/com/android/server/biometrics/OWNERS
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index d6292ca..d15aee0 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -266,6 +266,14 @@
     private static final int NOT_A_SUBTYPE_ID = -1;
 
     /**
+     * {@code true} to try to avoid blocking apps' UI thread by sending
+     * {@link StartInputReason#WINDOW_FOCUS_GAIN_REPORT_WITH_CONNECTION} and
+     * {@link StartInputReason#WINDOW_FOCUS_GAIN_REPORT_WITHOUT_CONNECTION} in a truly asynchronous
+     * way. {@code false} to go back to the previous synchronous semantics.
+     */
+    private static final boolean USE_REPORT_WINDOW_GAINED_FOCUS_ASYNC = false;
+
+    /**
      * A constant that represents Voice IME.
      *
      * @see InputMethodSubtype#getMode()
@@ -689,20 +697,29 @@
                         Log.v(TAG, "Reporting focus gain, without startInput"
                                 + ", nextFocusIsServedView=" + nextFocusHasConnection);
                     }
-                    final int startInputReason =
-                            nextFocusHasConnection ? WINDOW_FOCUS_GAIN_REPORT_WITH_CONNECTION
-                                    : WINDOW_FOCUS_GAIN_REPORT_WITHOUT_CONNECTION;
-                    final Completable.InputBindResult value = Completable.createInputBindResult();
-                    mService.startInputOrWindowGainedFocus(
-                            startInputReason, mClient,
-                            focusedView.getWindowToken(), startInputFlags, softInputMode,
-                            windowFlags,
-                            null,
-                            null,
-                            0 /* missingMethodFlags */,
-                            mCurRootView.mContext.getApplicationInfo().targetSdkVersion,
-                            ResultCallbacks.of(value));
-                    Completable.getResult(value); // ignore the result
+
+                    if (USE_REPORT_WINDOW_GAINED_FOCUS_ASYNC) {
+                        mService.reportWindowGainedFocusAsync(
+                                nextFocusHasConnection, mClient, focusedView.getWindowToken(),
+                                startInputFlags, softInputMode, windowFlags,
+                                mCurRootView.mContext.getApplicationInfo().targetSdkVersion);
+                    } else {
+                        final int startInputReason = nextFocusHasConnection
+                                ? WINDOW_FOCUS_GAIN_REPORT_WITH_CONNECTION
+                                : WINDOW_FOCUS_GAIN_REPORT_WITHOUT_CONNECTION;
+                        final Completable.InputBindResult value =
+                                Completable.createInputBindResult();
+                        mService.startInputOrWindowGainedFocus(
+                                startInputReason, mClient,
+                                focusedView.getWindowToken(), startInputFlags, softInputMode,
+                                windowFlags,
+                                null,
+                                null,
+                                0 /* missingMethodFlags */,
+                                mCurRootView.mContext.getApplicationInfo().targetSdkVersion,
+                                ResultCallbacks.of(value));
+                        Completable.getResult(value); // ignore the result
+                    }
                 } catch (RemoteException e) {
                     throw e.rethrowFromSystemServer();
                 }
@@ -1087,6 +1104,11 @@
         public void setImeTraceEnabled(boolean enabled) {
             ImeTracing.getInstance().setEnabled(enabled);
         }
+
+        @Override
+        public void throwExceptionFromSystem(String message) {
+            throw new RuntimeException(message);
+        }
     };
 
     final InputConnection mDummyInputConnection = new BaseInputConnection(this, false);
diff --git a/core/java/android/window/SplashScreen.java b/core/java/android/window/SplashScreen.java
index 18f29ae..7d222db 100644
--- a/core/java/android/window/SplashScreen.java
+++ b/core/java/android/window/SplashScreen.java
@@ -17,12 +17,17 @@
 package android.window;
 
 import android.annotation.NonNull;
+import android.annotation.StyleRes;
 import android.annotation.SuppressLint;
 import android.annotation.UiThread;
 import android.app.Activity;
 import android.app.ActivityThread;
+import android.app.AppGlobals;
 import android.content.Context;
+import android.content.res.Resources;
 import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
 import android.util.Singleton;
 import android.util.Slog;
 
@@ -60,6 +65,17 @@
      */
     void clearOnExitAnimationListener();
 
+
+    /**
+     * Overrides the theme used for the {@link SplashScreen}s of this application.
+     * <p>
+     * By default, the {@link SplashScreen} uses the theme set in the manifest. This method
+     * overrides and persists the theme used for the {@link SplashScreen} of this application.
+     * <p>
+     * To reset to the default theme, set this the themeId to {@link Resources#ID_NULL}.
+     */
+    void setSplashScreenTheme(@StyleRes int themeId);
+
     /**
      * Listens for the splash screen exit event.
      */
@@ -84,6 +100,8 @@
      * @hide
      */
     class SplashScreenImpl implements SplashScreen {
+        private static final String TAG = "SplashScreenImpl";
+
         private OnExitAnimationListener mExitAnimationListener;
         private final IBinder mActivityToken;
         private final SplashScreenManagerGlobal mGlobal;
@@ -119,6 +137,29 @@
                 mGlobal.removeImpl(this);
             }
         }
+
+        public void setSplashScreenTheme(@StyleRes int themeId) {
+            if (mActivityToken == null) {
+                Log.w(TAG, "Couldn't persist the starting theme. This instance is not an Activity");
+                return;
+            }
+
+            Activity activity = ActivityThread.currentActivityThread().getActivity(
+                    mActivityToken);
+            if (activity == null) {
+                return;
+            }
+            String themeName = themeId != Resources.ID_NULL
+                    ? activity.getResources().getResourceName(themeId) : null;
+
+            try {
+                AppGlobals.getPackageManager().setSplashScreenTheme(
+                        activity.getComponentName().getPackageName(),
+                        themeName, activity.getUserId());
+            } catch (RemoteException e) {
+                Log.w(TAG, "Couldn't persist the starting theme", e);
+            }
+        }
     }
 
     /**
diff --git a/core/java/com/android/internal/view/IInputMethodClient.aidl b/core/java/com/android/internal/view/IInputMethodClient.aidl
index 49dbbaa..a61e86b 100644
--- a/core/java/com/android/internal/view/IInputMethodClient.aidl
+++ b/core/java/com/android/internal/view/IInputMethodClient.aidl
@@ -30,4 +30,5 @@
     void reportFullscreenMode(boolean fullscreen);
     void updateActivityViewToScreenMatrix(int bindSequence, in float[] matrixValues);
     void setImeTraceEnabled(boolean enabled);
+    void throwExceptionFromSystem(String message);
 }
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 93cd4e9..772e344 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -68,6 +68,12 @@
             int unverifiedTargetSdkVersion,
             in IInputBindResultResultCallback inputBindResult);
 
+    oneway void reportWindowGainedFocusAsync(
+            boolean nextFocusHasConnection, in IInputMethodClient client, in IBinder windowToken,
+            /* @StartInputFlags */ int startInputFlags,
+            /* @android.view.WindowManager.LayoutParams.SoftInputModeFlags */ int softInputMode,
+            int windowFlags, int unverifiedTargetSdkVersion);
+
     oneway void showInputMethodPickerFromClient(in IInputMethodClient client,
             int auxiliarySubtypeMode, in IVoidResultCallback resultCallback);
     oneway void showInputMethodPickerFromSystem(in IInputMethodClient client,
diff --git a/graphics/java/android/graphics/drawable/VectorDrawable.java b/graphics/java/android/graphics/drawable/VectorDrawable.java
index 9298d9fc..4065bd1 100644
--- a/graphics/java/android/graphics/drawable/VectorDrawable.java
+++ b/graphics/java/android/graphics/drawable/VectorDrawable.java
@@ -349,15 +349,19 @@
     private final Rect mTmpBounds = new Rect();
 
     public VectorDrawable() {
-        this(new VectorDrawableState(null), null);
+        this(null, null);
     }
 
     /**
      * The one constructor to rule them all. This is called by all public
      * constructors to set the state and initialize local properties.
      */
-    private VectorDrawable(@NonNull VectorDrawableState state, @Nullable Resources res) {
-        mVectorState = state;
+    private VectorDrawable(@Nullable VectorDrawableState state, @Nullable Resources res) {
+        // As the mutable, not-thread-safe native instance is stored in VectorDrawableState, we
+        // need to always do a defensive copy even if mutate() isn't called. Otherwise
+        // draw() being called on 2 different VectorDrawable instances could still hit the same
+        // underlying native object.
+        mVectorState = new VectorDrawableState(state);
         updateLocalState(res);
     }
 
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
index 4142e51..b75252b 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
@@ -58,7 +58,6 @@
     /** Returns true if the gesture should be rejected. */
     boolean isFalseTouch(int interactionType);
 
-
     /**
      * Does basic checking to see if gesture looks like a tap.
      *
@@ -127,8 +126,19 @@
     /** Removes a {@link FalsingBeliefListener}. */
     void removeFalsingBeliefListener(FalsingBeliefListener listener);
 
+    /** Adds a {@link FalsingTapListener}. */
+    void addTapListener(FalsingTapListener falsingTapListener);
+
+    /** Removes a {@link FalsingTapListener}. */
+    void removeTapListener(FalsingTapListener falsingTapListener);
+
     /** Listener that is alerted when falsing belief level crosses a predfined threshold. */
     interface FalsingBeliefListener {
         void onFalse();
     }
+
+    /** Listener that is alerted when a double tap is required to confirm a single tap. */
+    interface FalsingTapListener {
+        void onDoubleTapRequired();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/OWNERS b/packages/SystemUI/src/com/android/systemui/biometrics/OWNERS
index 8765c9a..947466f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/OWNERS
@@ -1,7 +1,3 @@
 set noparent
 
-kchyn@google.com
-jaggies@google.com
-curtislb@google.com
-ilyamaty@google.com
-joshmccloskey@google.com
+include /services/core/java/com/android/server/biometrics/OWNERS
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
index 01d5959..9e621b3 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
@@ -79,6 +79,7 @@
 
     private final Collection<FalsingClassifier> mClassifiers;
     private final List<FalsingBeliefListener> mFalsingBeliefListeners = new ArrayList<>();
+    private List<FalsingTapListener> mFalsingTapListeners = new ArrayList<>();
 
     private final SessionListener mSessionListener = new SessionListener() {
         @Override
@@ -277,6 +278,7 @@
                         FalsingClassifier.Result.falsed(
                                 0, getClass().getSimpleName(), "bad history"));
                 logDebug("False Single Tap: true (bad history)");
+                mFalsingTapListeners.forEach(FalsingTapListener::onDoubleTapRequired);
                 return true;
             } else {
                 mPriorResults = Collections.singleton(FalsingClassifier.Result.passed(0.1));
@@ -356,6 +358,16 @@
     }
 
     @Override
+    public void addTapListener(FalsingTapListener listener) {
+        mFalsingTapListeners.add(listener);
+    }
+
+    @Override
+    public void removeTapListener(FalsingTapListener listener) {
+        mFalsingTapListeners.remove(listener);
+    }
+
+    @Override
     public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
         IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
         ipw.println("BRIGHTLINE FALSING MANAGER");
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java
index e557773..e8445d4 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java
@@ -146,4 +146,14 @@
     public void removeFalsingBeliefListener(FalsingBeliefListener listener) {
         mFalsingBeliefListeners.remove(listener);
     }
+
+    @Override
+    public void addTapListener(FalsingTapListener falsingTapListener) {
+
+    }
+
+    @Override
+    public void removeTapListener(FalsingTapListener falsingTapListener) {
+
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java
index 1723291..6b819fb 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java
@@ -176,6 +176,16 @@
     }
 
     @Override
+    public void addTapListener(FalsingTapListener listener) {
+        mInternalFalsingManager.addTapListener(listener);
+    }
+
+    @Override
+    public void removeTapListener(FalsingTapListener listener) {
+        mInternalFalsingManager.removeTapListener(listener);
+    }
+
+    @Override
     public void onProximityEvent(ThresholdSensor.ThresholdSensorEvent proximityEvent) {
         mInternalFalsingManager.onProximityEvent(proximityEvent);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 8264a9c..fb109f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -84,7 +84,6 @@
     private int mCutoutHeight;
     private int mGapHeight;
     private int mIndexOfFirstViewInShelf = -1;
-    private int mIndexOfFirstViewInOverflowingSection = -1;
 
     private NotificationShelfController mController;
 
@@ -180,7 +179,6 @@
             viewState.xTranslation = getTranslationX();
             viewState.hasItemsInStableShelf = lastViewState.inShelf;
             viewState.firstViewInShelf = algorithmState.firstViewInShelf;
-            viewState.firstViewInOverflowSection = algorithmState.firstViewInOverflowSection;
             if (mNotGoneIndex != -1) {
                 viewState.notGoneIndex = Math.min(viewState.notGoneIndex, mNotGoneIndex);
             }
@@ -268,17 +266,6 @@
             // TODO(b/172289889) scale mPaddingBetweenElements with expansion amount
             if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) {
                 notificationClipEnd = stackEnd;
-            } else if (mAmbientState.isExpansionChanging()) {
-                if (mIndexOfFirstViewInOverflowingSection != -1
-                    && i >= mIndexOfFirstViewInOverflowingSection) {
-                    // Clip notifications in (section overflowing into shelf) to shelf start.
-                    notificationClipEnd = shelfStart - mPaddingBetweenElements;
-                } else {
-                    // Clip notifications before the section overflowing into shelf
-                    // to stackEnd because we do not show the shelf if the section right before the
-                    // shelf is still hidden.
-                    notificationClipEnd = stackEnd;
-                }
             } else {
                 notificationClipEnd = shelfStart - mPaddingBetweenElements;
             }
@@ -831,11 +818,6 @@
         mIndexOfFirstViewInShelf = mHostLayoutController.indexOfChild(firstViewInShelf);
     }
 
-    public void setFirstViewInOverflowingSection(ExpandableView firstViewInOverflowingSection) {
-        mIndexOfFirstViewInOverflowingSection =
-                mHostLayoutController.indexOfChild(firstViewInOverflowingSection);
-    }
-
     private class ShelfState extends ExpandableViewState {
         private boolean hasItemsInStableShelf;
         private ExpandableView firstViewInShelf;
@@ -849,7 +831,6 @@
 
             super.applyToView(view);
             setIndexOfFirstViewInShelf(firstViewInShelf);
-            setFirstViewInOverflowingSection(firstViewInOverflowSection);
             updateAppearance();
             setHasItemsInStableShelf(hasItemsInStableShelf);
             mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
@@ -863,7 +844,6 @@
 
             super.animateTo(child, properties);
             setIndexOfFirstViewInShelf(firstViewInShelf);
-            setFirstViewInOverflowingSection(firstViewInOverflowSection);
             updateAppearance();
             setHasItemsInStableShelf(hasItemsInStableShelf);
             mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 6cacec7..e3c5546 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -33,9 +33,7 @@
 import com.android.systemui.statusbar.notification.row.FooterView;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 /**
  * The Algorithm of the {@link com.android.systemui.statusbar.notification.stack
@@ -255,16 +253,16 @@
             }
         }
 
-        state.firstViewInShelf = null;
-        // Save y, sectionStart, sectionEnd from when shade is fully expanded.
-        // Consider updating these states in updateContentView instead so that we don't have to
-        // recalculate in every frame.
+        // Save (height of view before shelf, index of first view in shelf) from when shade is fully
+        // expanded. Consider updating these states in updateContentView instead so that we don't
+        // have to recalculate in every frame.
         float currentY = -scrollY;
-        int sectionStartIndex = 0;
-        int sectionEndIndex = 0;
+        float previousY = 0;
+        state.firstViewInShelf = null;
+        state.viewHeightBeforeShelf = -1;
         for (int i = 0; i < state.visibleChildren.size(); i++) {
             final ExpandableView view = state.visibleChildren.get(i);
-            // Add space between sections.
+
             final boolean applyGapHeight = childNeedsGapHeight(
                     ambientState.getSectionProvider(), i,
                     view, getPreviousView(i, state));
@@ -273,88 +271,27 @@
             }
 
             if (ambientState.getShelf() != null) {
-                // Save index of first view in the shelf
                 final float shelfStart = ambientState.getStackEndHeight()
                         - ambientState.getShelf().getIntrinsicHeight();
                 if (currentY >= shelfStart
                         && !(view instanceof FooterView)
                         && state.firstViewInShelf == null) {
                     state.firstViewInShelf = view;
+                    // There might be a section gap right before the shelf.
+                    // Limit the height of the view before the shelf so that it does not include
+                    // a gap and become taller than it normally is.
+                    state.viewHeightBeforeShelf = Math.min(getMaxAllowedChildHeight(view),
+                            ambientState.getStackEndHeight()
+                            - ambientState.getShelf().getIntrinsicHeight()
+                            - mPaddingBetweenElements
+                            - previousY);
                 }
             }
-
-            // Record y position when fully expanded
-            ExpansionData expansionData = new ExpansionData();
-            expansionData.fullyExpandedY = currentY;
-            state.expansionData.put(view, expansionData);
-
-            if (ambientState.getSectionProvider()
-                    .beginsSection(view, getPreviousView(i, state))) {
-
-                // Save section start/end for views in the section before this new section
-                ExpandableView sectionStartView = state.visibleChildren.get(sectionStartIndex);
-                final float sectionStart =
-                        state.expansionData.get(sectionStartView).fullyExpandedY;
-
-                ExpandableView sectionEndView = state.visibleChildren.get(sectionEndIndex);
-                float sectionEnd = state.expansionData.get(sectionEndView).fullyExpandedY
-                        + sectionEndView.getIntrinsicHeight();
-
-                if (ambientState.getShelf() != null) {
-                    // If we show the shelf, trim section end to shelf start
-                    // This means section end > start for views in the shelf
-                    final float shelfStart = ambientState.getStackEndHeight()
-                            - ambientState.getShelf().getIntrinsicHeight();
-                    if (state.firstViewInShelf != null && sectionEnd > shelfStart) {
-                        sectionEnd = shelfStart;
-                    }
-                }
-
-                // Update section bounds of every view in the previous section
-                // Consider using shared SectionInfo for views in same section to avoid looping back
-                for (int j = sectionStartIndex; j < i; j++) {
-                    ExpandableView sectionView = state.visibleChildren.get(j);
-                    ExpansionData viewExpansionData =
-                            state.expansionData.get(sectionView);
-                    viewExpansionData.sectionStart = sectionStart;
-                    viewExpansionData.sectionEnd = sectionEnd;
-                    state.expansionData.put(sectionView, viewExpansionData);
-                }
-                sectionStartIndex = i;
-
-                if (view instanceof FooterView) {
-                    // Also record section bounds for FooterView (same as its own)
-                    // because it is the last view and we won't get to this point again
-                    // after the loop ends
-                    ExpansionData footerExpansionData = state.expansionData.get(view);
-                    footerExpansionData.sectionStart = expansionData.fullyExpandedY;
-                    footerExpansionData.sectionEnd = expansionData.fullyExpandedY
-                            + view.getIntrinsicHeight();
-                    state.expansionData.put(view, footerExpansionData);
-                }
-            }
-            sectionEndIndex = i;
+            previousY = currentY;
             currentY = currentY
                     + getMaxAllowedChildHeight(view)
                     + mPaddingBetweenElements;
         }
-
-        // Which view starts the section of the view right before the shelf?
-        // Save it for later when we clip views in that section to shelf start.
-        state.firstViewInOverflowSection = null;
-        if (state.firstViewInShelf != null) {
-            ExpandableView nextView = null;
-            final int startIndex = state.visibleChildren.indexOf(state.firstViewInShelf);
-            for (int i = startIndex - 1; i >= 0; i--) {
-                ExpandableView view = state.visibleChildren.get(i);
-                if (nextView != null && ambientState.getSectionProvider()
-                        .beginsSection(nextView, view)) {
-                    break;
-                }
-                nextView = view;
-            }
-            state.firstViewInOverflowSection = nextView;
-        }
     }
 
     private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex,
@@ -394,6 +331,26 @@
         }
     }
 
+    /**
+     * @return Fraction to apply to view height and gap between views.
+     *         Does not include shelf height even if shelf is showing.
+     */
+    private float getExpansionFractionWithoutShelf(
+            StackScrollAlgorithmState algorithmState,
+            AmbientState ambientState) {
+
+        final boolean isShowingShelf = ambientState.getShelf() != null
+                && algorithmState.firstViewInShelf != null;
+
+        final float stackHeight = ambientState.getStackHeight()
+                - (isShowingShelf ? ambientState.getShelf().getIntrinsicHeight() : 0f);
+
+        float stackEndHeight = ambientState.getStackEndHeight()
+                - (isShowingShelf ? ambientState.getShelf().getIntrinsicHeight() : 0f);
+
+        return stackHeight / stackEndHeight;
+    }
+
     // TODO(b/172289889) polish shade open from HUN
     /**
      * Populates the {@link ExpandableViewState} for a single child.
@@ -426,22 +383,8 @@
             viewState.headsUpIsVisible = end < ambientState.getMaxHeadsUpTranslation();
         }
 
-        // TODO(b/172289889) move sectionFraction and showSection to initAlgorithmState
-        // Get fraction of section showing, and later apply it to view height and gaps between views
-        float sectionFraction = 1f;
-        boolean showSection = true;
-
-        if (!ambientState.isOnKeyguard()
-                && !ambientState.isPulseExpanding()
-                && ambientState.isExpansionChanging()) {
-
-            final ExpansionData expansionData = algorithmState.expansionData.get(view);
-            final float sectionHeight = expansionData.sectionEnd - expansionData.sectionStart;
-            sectionFraction = MathUtils.constrain(
-                    (ambientState.getStackHeight() - expansionData.sectionStart) / sectionHeight,
-                    0f, 1f);
-            showSection = expansionData.sectionStart < ambientState.getStackHeight();
-        }
+        final float expansionFraction = getExpansionFractionWithoutShelf(
+                algorithmState, ambientState);
 
         // Add gap between sections.
         final boolean applyGapHeight =
@@ -449,46 +392,58 @@
                         ambientState.getSectionProvider(), i,
                         view, getPreviousView(i, algorithmState));
         if (applyGapHeight) {
-            currentYPosition += sectionFraction * mGapHeight;
+            currentYPosition += expansionFraction * mGapHeight;
         }
 
         viewState.yTranslation = currentYPosition;
-
         if (view instanceof SectionHeaderView) {
             // Add padding before sections for overscroll effect.
-            viewState.yTranslation += ambientState.getSectionPadding();
+            viewState.yTranslation += expansionFraction * ambientState.getSectionPadding();
         }
 
-        if (view != ambientState.getTrackedHeadsUpRow()) {
+        if (view instanceof FooterView) {
+            viewState.yTranslation = Math.min(viewState.yTranslation,
+                    ambientState.getStackHeight());
+            // Hide footer if shelf is showing
+            viewState.hidden = algorithmState.firstViewInShelf != null;
+        } else if (view != ambientState.getTrackedHeadsUpRow()) {
             if (ambientState.isExpansionChanging()) {
-                viewState.hidden = !showSection;
+                // Show all views. Views below the shelf will later be clipped (essentially hidden)
+                // in NotificationShelf.
+                viewState.hidden = false;
                 viewState.inShelf = algorithmState.firstViewInShelf != null
                         && i >= algorithmState.visibleChildren.indexOf(
-                                algorithmState.firstViewInShelf)
-                        && !(view instanceof FooterView);
+                                algorithmState.firstViewInShelf);
             } else if (ambientState.getShelf() != null) {
                 // When pulsing (incoming notification on AOD), innerHeight is 0; clamp all
                 // to shelf start, thereby hiding all notifications (except the first one, which we
                 // later unhide in updatePulsingState)
                 final int shelfStart = ambientState.getInnerHeight()
                         - ambientState.getShelf().getIntrinsicHeight();
-                if (!(view instanceof FooterView)) {
-                    viewState.yTranslation = Math.min(viewState.yTranslation, shelfStart);
-                }
+                viewState.yTranslation = Math.min(viewState.yTranslation, shelfStart);
                 if (viewState.yTranslation >= shelfStart) {
                     viewState.hidden = !view.isExpandAnimationRunning()
-                            && !view.hasExpandingChild()
-                            && !(view instanceof FooterView);
+                            && !view.hasExpandingChild();
                     viewState.inShelf = true;
                     // Notifications in the shelf cannot be visible HUNs.
                     viewState.headsUpIsVisible = false;
                 }
             }
-            viewState.height = (int) MathUtils.lerp(
-                    0, getMaxAllowedChildHeight(view), sectionFraction);
+
+            // Clip height of view right before shelf.
+            float maxViewHeight = getMaxAllowedChildHeight(view);
+            if (ambientState.isExpansionChanging()
+                    && algorithmState.viewHeightBeforeShelf != -1) {
+                final int indexOfFirstViewInShelf = algorithmState.visibleChildren.indexOf(
+                        algorithmState.firstViewInShelf);
+                if (i == indexOfFirstViewInShelf - 1) {
+                    maxViewHeight = algorithmState.viewHeightBeforeShelf;
+                }
+            }
+            viewState.height = (int) MathUtils.lerp(0, maxViewHeight, expansionFraction);
         }
 
-        currentYPosition += viewState.height + sectionFraction * mPaddingBetweenElements;
+        currentYPosition += viewState.height + expansionFraction * mPaddingBetweenElements;
         setLocation(view.getViewState(), currentYPosition, i);
         viewState.yTranslation += ambientState.getStackY();
         return currentYPosition;
@@ -743,35 +698,6 @@
         this.mIsExpanded = isExpanded;
     }
 
-    /**
-     * Data used to layout views while shade expansion changes.
-     */
-    public class ExpansionData {
-
-        /**
-         * Y position of top of first view in section.
-         */
-        public float sectionStart;
-
-        /**
-         * Y position of bottom of last view in section.
-         */
-        public float sectionEnd;
-
-        /**
-         * Y position of view when shade is fully expanded.
-         * Does not include distance between top notifications panel and top of screen.
-         */
-        public float fullyExpandedY;
-
-        /**
-         * Whether this notification is in the same section as the notification right before the
-         * shelf. Used to determine which notification should be clipped to shelf start while
-         * shade expansion changes.
-         */
-        public boolean inOverflowingSection;
-    }
-
     public class StackScrollAlgorithmState {
 
         /**
@@ -785,16 +711,9 @@
         public ExpandableView firstViewInShelf;
 
         /**
-         * First view in section overflowing into shelf while shade expansion changes.
+         * Height of view right before the shelf.
          */
-        public ExpandableView firstViewInOverflowSection;
-
-        /**
-         * Map of view to ExpansionData used for layout during shade expansion.
-         * Use view instead of index as key, because visibleChildren indices do not match the ones
-         * used in the shelf.
-         */
-        public Map<ExpandableView, ExpansionData> expansionData = new HashMap<>();
+        public float viewHeightBeforeShelf;
 
         /**
          * The children from the host view which are not gone.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index e2af940..9449836 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -53,6 +53,7 @@
 import android.os.PowerManager;
 import android.os.SystemClock;
 import android.os.UserManager;
+import android.os.VibrationEffect;
 import android.service.quickaccesswallet.QuickAccessWalletClient;
 import android.util.Log;
 import android.util.MathUtils;
@@ -101,6 +102,7 @@
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.media.MediaHierarchyManager;
 import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.FalsingManager.FalsingTapListener;
 import com.android.systemui.plugins.qs.DetailAdapter;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -206,6 +208,7 @@
     private final ExpansionCallback mExpansionCallback = new ExpansionCallback();
     private final BiometricUnlockController mBiometricUnlockController;
     private final NotificationPanelView mView;
+    private final VibratorHelper mVibratorHelper;
     private final MetricsLogger mMetricsLogger;
     private final ActivityManager mActivityManager;
     private final ConfigurationController mConfigurationController;
@@ -545,6 +548,14 @@
         }
     };
 
+    private final FalsingTapListener mFalsingTapListener = new FalsingTapListener() {
+        @Override
+        public void onDoubleTapRequired() {
+            showTransientIndication(R.string.notification_tap_again);
+            mVibratorHelper.vibrate(VibrationEffect.EFFECT_STRENGTH_MEDIUM);
+        }
+    };
+
     @Inject
     public NotificationPanelViewController(NotificationPanelView view,
             @Main Resources resources,
@@ -592,6 +603,7 @@
                 statusBarKeyguardViewManager, latencyTracker, flingAnimationUtilsBuilder.get(),
                 statusBarTouchableRegionManager, ambientState);
         mView = view;
+        mVibratorHelper = vibratorHelper;
         mMetricsLogger = metricsLogger;
         mActivityManager = activityManager;
         mConfigurationController = configurationController;
@@ -4015,6 +4027,7 @@
             // window, so
             // force a call to onThemeChanged
             mConfigurationListener.onThemeChanged();
+            mFalsingManager.addTapListener(mFalsingTapListener);
         }
 
         @Override
@@ -4023,6 +4036,7 @@
             mStatusBarStateController.removeCallback(mStatusBarStateListener);
             mConfigurationController.removeCallback(mConfigurationListener);
             mUpdateMonitor.removeCallback(mKeyguardUpdateCallback);
+            mFalsingManager.removeTapListener(mFalsingTapListener);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 088f947..2b5caf9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -364,7 +364,6 @@
                 MetricsEvent.ACTION_LS_NOTE,
                 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */);
         mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_NOTIFICATION_FALSE_TOUCH);
-        mNotificationPanel.showTransientIndication(R.string.notification_tap_again);
         ActivatableNotificationView previousView = mNotificationPanel.getActivatedChild();
         if (previousView != null) {
             previousView.makeInactive(true /* animate */);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 3424821..c7c681b 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -3355,68 +3355,100 @@
 
     @NonNull
     @Override
+    public void reportWindowGainedFocusAsync(
+            boolean nextFocusHasConnection, IInputMethodClient client, IBinder windowToken,
+            @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode,
+            int windowFlags, int unverifiedTargetSdkVersion) {
+        final int startInputReason = nextFocusHasConnection
+                ? StartInputReason.WINDOW_FOCUS_GAIN_REPORT_WITH_CONNECTION
+                : StartInputReason.WINDOW_FOCUS_GAIN_REPORT_WITHOUT_CONNECTION;
+        try {
+            startInputOrWindowGainedFocusInternal(startInputReason, client, windowToken,
+                    startInputFlags, softInputMode, windowFlags, null /* attribute */,
+                    null /* inputContext */, 0 /* missingMethods */, unverifiedTargetSdkVersion);
+        } catch (Throwable t) {
+            if (client != null) {
+                try {
+                    client.throwExceptionFromSystem(t.getMessage());
+                } catch (RemoteException ignore) { }
+            }
+        }
+    }
+
+    @NonNull
+    @Override
     public void startInputOrWindowGainedFocus(
             @StartInputReason int startInputReason, IInputMethodClient client, IBinder windowToken,
             @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode,
             int windowFlags, @Nullable EditorInfo attribute, IInputContext inputContext,
             @MissingMethodFlags int missingMethods, int unverifiedTargetSdkVersion,
             IInputBindResultResultCallback resultCallback) {
-        CallbackUtils.onResult(resultCallback, (Supplier<InputBindResult>) () -> {
-            if (windowToken == null) {
-                Slog.e(TAG, "windowToken cannot be null.");
+        CallbackUtils.onResult(resultCallback, (Supplier<InputBindResult>) () ->
+                startInputOrWindowGainedFocusInternal(startInputReason, client, windowToken,
+                startInputFlags, softInputMode, windowFlags, attribute, inputContext,
+                missingMethods, unverifiedTargetSdkVersion));
+    }
+
+    @NonNull
+    private InputBindResult startInputOrWindowGainedFocusInternal(
+            @StartInputReason int startInputReason, IInputMethodClient client, IBinder windowToken,
+            @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode,
+            int windowFlags, @Nullable EditorInfo attribute, @Nullable IInputContext inputContext,
+            @MissingMethodFlags int missingMethods, int unverifiedTargetSdkVersion) {
+        if (windowToken == null) {
+            Slog.e(TAG, "windowToken cannot be null.");
+            return InputBindResult.NULL;
+        }
+        try {
+            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
+                    "IMMS.startInputOrWindowGainedFocus");
+            ImeTracing.getInstance().triggerManagerServiceDump(
+                    "InputMethodManagerService#startInputOrWindowGainedFocus");
+            final int callingUserId = UserHandle.getCallingUserId();
+            final int userId;
+            if (attribute != null && attribute.targetInputMethodUser != null
+                    && attribute.targetInputMethodUser.getIdentifier() != callingUserId) {
+                mContext.enforceCallingPermission(
+                        Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+                        "Using EditorInfo.targetInputMethodUser requires"
+                                + " INTERACT_ACROSS_USERS_FULL.");
+                userId = attribute.targetInputMethodUser.getIdentifier();
+                if (!mUserManagerInternal.isUserRunning(userId)) {
+                    // There is a chance that we hit here because of race condition. Let's just
+                    // return an error code instead of crashing the caller process, which at
+                    // least has INTERACT_ACROSS_USERS_FULL permission thus is likely to be an
+                    // important process.
+                    Slog.e(TAG, "User #" + userId + " is not running.");
+                    return InputBindResult.INVALID_USER;
+                }
+            } else {
+                userId = callingUserId;
+            }
+            final InputBindResult result;
+            synchronized (mMethodMap) {
+                final long ident = Binder.clearCallingIdentity();
+                try {
+                    result = startInputOrWindowGainedFocusInternalLocked(startInputReason,
+                            client, windowToken, startInputFlags, softInputMode, windowFlags,
+                            attribute, inputContext, missingMethods, unverifiedTargetSdkVersion,
+                            userId);
+                } finally {
+                    Binder.restoreCallingIdentity(ident);
+                }
+            }
+            if (result == null) {
+                // This must never happen, but just in case.
+                Slog.wtf(TAG, "InputBindResult is @NonNull. startInputReason="
+                        + InputMethodDebug.startInputReasonToString(startInputReason)
+                        + " windowFlags=#" + Integer.toHexString(windowFlags)
+                        + " editorInfo=" + attribute);
                 return InputBindResult.NULL;
             }
-            try {
-                Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
-                        "IMMS.startInputOrWindowGainedFocus");
-                ImeTracing.getInstance().triggerManagerServiceDump(
-                        "InputMethodManagerService#startInputOrWindowGainedFocus");
-                final int callingUserId = UserHandle.getCallingUserId();
-                final int userId;
-                if (attribute != null && attribute.targetInputMethodUser != null
-                        && attribute.targetInputMethodUser.getIdentifier() != callingUserId) {
-                    mContext.enforceCallingPermission(
-                            Manifest.permission.INTERACT_ACROSS_USERS_FULL,
-                            "Using EditorInfo.targetInputMethodUser requires"
-                                    + " INTERACT_ACROSS_USERS_FULL.");
-                    userId = attribute.targetInputMethodUser.getIdentifier();
-                    if (!mUserManagerInternal.isUserRunning(userId)) {
-                        // There is a chance that we hit here because of race condition. Let's just
-                        // return an error code instead of crashing the caller process, which at
-                        // least has INTERACT_ACROSS_USERS_FULL permission thus is likely to be an
-                        // important process.
-                        Slog.e(TAG, "User #" + userId + " is not running.");
-                        return InputBindResult.INVALID_USER;
-                    }
-                } else {
-                    userId = callingUserId;
-                }
-                final InputBindResult result;
-                synchronized (mMethodMap) {
-                    final long ident = Binder.clearCallingIdentity();
-                    try {
-                        result = startInputOrWindowGainedFocusInternalLocked(startInputReason,
-                                client, windowToken, startInputFlags, softInputMode, windowFlags,
-                                attribute, inputContext, missingMethods, unverifiedTargetSdkVersion,
-                                userId);
-                    } finally {
-                        Binder.restoreCallingIdentity(ident);
-                    }
-                }
-                if (result == null) {
-                    // This must never happen, but just in case.
-                    Slog.wtf(TAG, "InputBindResult is @NonNull. startInputReason="
-                            + InputMethodDebug.startInputReasonToString(startInputReason)
-                            + " windowFlags=#" + Integer.toHexString(windowFlags)
-                            + " editorInfo=" + attribute);
-                    return InputBindResult.NULL;
-                }
 
-                return result;
-            } finally {
-                Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
-            }
-        });
+            return result;
+        } finally {
+            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+        }
     }
 
     @NonNull
diff --git a/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java
index fcaf6d8..0fc91ba 100644
--- a/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java
@@ -1627,6 +1627,33 @@
 
         @BinderThread
         @Override
+        public void reportWindowGainedFocusAsync(
+                boolean nextFocusHasConnection,
+                @Nullable IInputMethodClient client,
+                @Nullable IBinder windowToken,
+                @StartInputFlags int startInputFlags,
+                @SoftInputModeFlags int softInputMode,
+                int windowFlags,
+                int unverifiedTargetSdkVersion) {
+            final int startInputReason = nextFocusHasConnection
+                    ? StartInputReason.WINDOW_FOCUS_GAIN_REPORT_WITH_CONNECTION
+                    : StartInputReason.WINDOW_FOCUS_GAIN_REPORT_WITHOUT_CONNECTION;
+            try {
+                startInputOrWindowGainedFocusInternal(startInputReason, client, windowToken,
+                        startInputFlags, softInputMode, windowFlags, null /* editorInfo */,
+                        null /* inputContext */, 0 /* missingMethods */,
+                        unverifiedTargetSdkVersion);
+            } catch (Throwable t) {
+                if (client != null) {
+                    try {
+                        client.throwExceptionFromSystem(t.getMessage());
+                    } catch (RemoteException ignore) { }
+                }
+            }
+        }
+
+        @BinderThread
+        @Override
         public void startInputOrWindowGainedFocus(
                 @StartInputReason int startInputReason,
                 @Nullable IInputMethodClient client,
@@ -1641,8 +1668,8 @@
                 IInputBindResultResultCallback resultCallback) {
             CallbackUtils.onResult(resultCallback, (Supplier<InputBindResult>) () ->
                     startInputOrWindowGainedFocusInternal(startInputReason, client, windowToken,
-                            startInputFlags, softInputMode, windowFlags, editorInfo, inputContext,
-                            missingMethods, unverifiedTargetSdkVersion));
+                    startInputFlags, softInputMode, windowFlags, editorInfo, inputContext,
+                    missingMethods, unverifiedTargetSdkVersion));
         }
 
         @BinderThread
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index f5f0ce1..8d6c145 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -21627,7 +21627,8 @@
                     null /*disabledComponents*/,
                     PackageManager.INSTALL_REASON_UNKNOWN,
                     PackageManager.UNINSTALL_REASON_UNKNOWN,
-                    null /*harmfulAppWarning*/);
+                    null /*harmfulAppWarning*/,
+                    null /*splashScreenTheme*/);
         }
         mSettings.writeKernelMappingLPr(ps);
     }
@@ -27701,6 +27702,23 @@
         return mSettings.getPackageLPr(packageName).getMimeGroup(mimeGroup);
     }
 
+    @Override
+    public void setSplashScreenTheme(@NonNull String packageName, @Nullable String themeId,
+            int userId) {
+        int callingUid = Binder.getCallingUid();
+        PackageSetting packageSetting = getPackageSettingForUser(packageName, callingUid, userId);
+        if (packageSetting != null) {
+            packageSetting.setSplashScreenTheme(userId, themeId);
+        }
+    }
+
+    @Override
+    public String getSplashScreenTheme(@NonNull String packageName, int userId) {
+        int callingUid = Binder.getCallingUid();
+        PackageSetting packageSetting = getPackageSettingForUser(packageName, callingUid, userId);
+        return packageSetting != null ? packageSetting.getSplashScreenTheme(userId) : null;
+    }
+
     /**
      * Temporary method that wraps mSettings.writeLPr() and calls mPermissionManager's
      * writeLegacyPermissionsTEMP() beforehand.
diff --git a/services/core/java/com/android/server/pm/PackageSettingBase.java b/services/core/java/com/android/server/pm/PackageSettingBase.java
index 19b56b7..731d41c 100644
--- a/services/core/java/com/android/server/pm/PackageSettingBase.java
+++ b/services/core/java/com/android/server/pm/PackageSettingBase.java
@@ -493,7 +493,8 @@
             ArrayMap<String, PackageUserState.SuspendParams> suspendParams, boolean instantApp,
             boolean virtualPreload, String lastDisableAppCaller,
             ArraySet<String> enabledComponents, ArraySet<String> disabledComponents,
-            int installReason, int uninstallReason, String harmfulAppWarning) {
+            int installReason, int uninstallReason, String harmfulAppWarning,
+            String splashScreenTheme) {
         PackageUserState state = modifyUserState(userId);
         state.ceDataInode = ceDataInode;
         state.enabled = enabled;
@@ -512,6 +513,7 @@
         state.instantApp = instantApp;
         state.virtualPreload = virtualPreload;
         state.harmfulAppWarning = harmfulAppWarning;
+        state.splashScreenTheme = splashScreenTheme;
         onChanged();
     }
 
@@ -522,7 +524,8 @@
                 otherState.instantApp,
                 otherState.virtualPreload, otherState.lastDisableAppCaller,
                 otherState.enabledComponents, otherState.disabledComponents,
-                otherState.installReason, otherState.uninstallReason, otherState.harmfulAppWarning);
+                otherState.installReason, otherState.uninstallReason, otherState.harmfulAppWarning,
+                otherState.splashScreenTheme);
     }
 
     ArraySet<String> getEnabledComponents(int userId) {
@@ -723,6 +726,26 @@
     }
 
     /**
+     * @param userId    the specified user to modify the theme for
+     * @param themeName the theme name to persist
+     * @see android.window.SplashScreen#setSplashScreenTheme(int)
+     */
+    public void setSplashScreenTheme(@UserIdInt int userId, @Nullable String themeName) {
+        modifyUserState(userId).splashScreenTheme = themeName;
+    }
+
+    /**
+     * @param userId the specified user to get the theme setting from
+     * @return the theme name previously persisted for the user or null
+     * if no splashscreen theme is persisted.
+     * @see android.window.SplashScreen#setSplashScreenTheme(int)
+     */
+    @Nullable
+    public String getSplashScreenTheme(@UserIdInt int userId) {
+        return readUserState(userId).splashScreenTheme;
+    }
+
+    /**
      * @return True if package is still being loaded, false if the package is fully loaded.
      */
     public boolean isPackageLoading() {
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index e409019..b6d4a5b 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -340,6 +340,7 @@
     private static final String ATTR_INSTANT_APP = "instant-app";
     private static final String ATTR_VIRTUAL_PRELOAD = "virtual-preload";
     private static final String ATTR_HARMFUL_APP_WARNING = "harmful-app-warning";
+    private static final String ATTR_SPLASH_SCREEN_THEME = "splash-screen-theme";
 
     private static final String ATTR_PACKAGE_NAME = "packageName";
     private static final String ATTR_FINGERPRINT = "fingerprint";
@@ -939,7 +940,9 @@
                                 null /*disabledComponents*/,
                                 PackageManager.INSTALL_REASON_UNKNOWN,
                                 PackageManager.UNINSTALL_REASON_UNKNOWN,
-                                null /*harmfulAppWarning*/);
+                                null, /*harmfulAppWarning*/
+                                null /*splashscreenTheme*/
+                        );
                     }
                 }
             }
@@ -1578,7 +1581,8 @@
                                 null /*disabledComponents*/,
                                 PackageManager.INSTALL_REASON_UNKNOWN,
                                 PackageManager.UNINSTALL_REASON_UNKNOWN,
-                                null /*harmfulAppWarning*/);
+                                null /*harmfulAppWarning*/,
+                                null /* splashScreenTheme*/);
                     }
                     return;
                 }
@@ -1666,6 +1670,8 @@
                             PackageManager.INSTALL_REASON_UNKNOWN);
                     final int uninstallReason = parser.getAttributeInt(null, ATTR_UNINSTALL_REASON,
                             PackageManager.UNINSTALL_REASON_UNKNOWN);
+                    final String splashScreenTheme = parser.getAttributeValue(null,
+                            ATTR_SPLASH_SCREEN_THEME);
 
                     ArraySet<String> enabledComponents = null;
                     ArraySet<String> disabledComponents = null;
@@ -1738,7 +1744,8 @@
                     ps.setUserState(userId, ceDataInode, enabled, installed, stopped, notLaunched,
                             hidden, distractionFlags, suspended, suspendParamsMap,
                             instantApp, virtualPreload, enabledCaller, enabledComponents,
-                            disabledComponents, installReason, uninstallReason, harmfulAppWarning);
+                            disabledComponents, installReason, uninstallReason, harmfulAppWarning,
+                            splashScreenTheme);
 
                     mDomainVerificationManager.setLegacyUserState(name, userId, verifState);
                 } else if (tagName.equals("preferred-activities")) {
@@ -1995,6 +2002,10 @@
                     serializer.attribute(null, ATTR_HARMFUL_APP_WARNING,
                             ustate.harmfulAppWarning);
                 }
+                if (ustate.splashScreenTheme != null) {
+                    serializer.attribute(null, ATTR_SPLASH_SCREEN_THEME,
+                            ustate.splashScreenTheme);
+                }
                 if (ustate.suspended) {
                     for (int i = 0; i < ustate.suspendParams.size(); i++) {
                         final String suspendingPackage = ustate.suspendParams.keyAt(i);
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 721432e..d4707d6f 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -176,12 +176,14 @@
 import android.app.servertransaction.PauseActivityItem;
 import android.app.servertransaction.ResumeActivityItem;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.Matrix;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -6677,8 +6679,30 @@
                         prev = null;
                     }
                 }
-                final int splashScreenThemeResId = options != null
+
+                // TODO(185200798): Persist theme name instead of theme if
+                int splashScreenThemeResId = options != null
                         ? options.getSplashScreenThemeResId() : 0;
+
+                // User can override the splashscreen theme. The theme name is used to persist
+                // the setting, so if no theme is set in the ActivityOptions, we check if has
+                // been persisted here.
+                if (splashScreenThemeResId == 0) {
+                    try {
+                        String themeName = mAtmService.getPackageManager()
+                                .getSplashScreenTheme(r.packageName, r.mUserId);
+                        if (themeName != null) {
+                            Context packageContext = mAtmService.mContext
+                                    .createPackageContext(r.packageName, 0);
+                            splashScreenThemeResId = packageContext.getResources()
+                                    .getIdentifier(themeName, null, null);
+                        }
+                    } catch (RemoteException | PackageManager.NameNotFoundException
+                            | Resources.NotFoundException ignore) {
+                        // Just use the default theme
+                    }
+                }
+
                 r.showStartingWindow(prev, newTask, isTaskSwitch(r, focusedTopActivity),
                         splashScreenThemeResId, samePackage);
             }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index d65a28d..8274e38 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -16771,7 +16771,9 @@
                     provisioningParams.isKeepAccountMigrated(), callerPackage);
 
             if (provisioningParams.isOrganizationOwnedProvisioning()) {
-                setProfileOwnerOnOrgOwnedDeviceState(admin, userInfo.id, caller.getUserId());
+                synchronized (getLockObject()) {
+                    markProfileOwnerOnOrganizationOwnedDeviceUncheckedLocked(admin, userInfo.id);
+                }
             }
 
             return userInfo.getUserHandle();
@@ -17003,22 +17005,6 @@
         }
     }
 
-    private void setProfileOwnerOnOrgOwnedDeviceState(
-            ComponentName admin, @UserIdInt int profileId, @UserIdInt int parentUserId) {
-        synchronized (getLockObject()) {
-            markProfileOwnerOnOrganizationOwnedDeviceUncheckedLocked(admin, profileId);
-        }
-        restrictRemovalOfManagedProfile(parentUserId);
-    }
-
-    private void restrictRemovalOfManagedProfile(@UserIdInt int parentUserId) {
-        final UserHandle parentUserHandle = UserHandle.of(parentUserId);
-        mUserManager.setUserRestriction(
-                UserManager.DISALLOW_REMOVE_MANAGED_PROFILE,
-                /* value= */ true,
-                parentUserHandle);
-    }
-
     @Override
     public void provisionFullyManagedDevice(
             @NonNull FullyManagedDeviceProvisioningParams provisioningParams,
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index 4ce336d..9b42799 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -2660,9 +2660,6 @@
     }
 
     switch (targetStatus) {
-        case IDataLoaderStatusListener::DATA_LOADER_UNAVAILABLE:
-            // Do nothing, this is a reset state.
-            break;
         case IDataLoaderStatusListener::DATA_LOADER_DESTROYED: {
             switch (currentStatus) {
                 case IDataLoaderStatusListener::DATA_LOADER_BINDING:
@@ -2683,8 +2680,12 @@
         }
         case IDataLoaderStatusListener::DATA_LOADER_CREATED:
             switch (currentStatus) {
-                case IDataLoaderStatusListener::DATA_LOADER_DESTROYED:
                 case IDataLoaderStatusListener::DATA_LOADER_UNAVAILABLE:
+                case IDataLoaderStatusListener::DATA_LOADER_UNRECOVERABLE:
+                    // Before binding need to make sure we are unbound.
+                    // Otherwise we'll get stuck binding.
+                    return destroy();
+                case IDataLoaderStatusListener::DATA_LOADER_DESTROYED:
                 case IDataLoaderStatusListener::DATA_LOADER_BINDING:
                     return bind();
                 case IDataLoaderStatusListener::DATA_LOADER_BOUND:
@@ -2709,7 +2710,8 @@
                    << ", but got: " << mountId;
         return binder::Status::fromServiceSpecificError(-EPERM, "Mount ID mismatch.");
     }
-    if (newStatus == IDataLoaderStatusListener::DATA_LOADER_UNRECOVERABLE) {
+    if (newStatus == IDataLoaderStatusListener::DATA_LOADER_UNAVAILABLE ||
+        newStatus == IDataLoaderStatusListener::DATA_LOADER_UNRECOVERABLE) {
         // User-provided status, let's postpone the handling to avoid possible deadlocks.
         mService.addTimedJob(*mService.mTimedQueue, id(), Constants::userStatusDelay,
                              [this, newStatus]() { setCurrentStatus(newStatus); });
@@ -2721,7 +2723,7 @@
 }
 
 void IncrementalService::DataLoaderStub::setCurrentStatus(int newStatus) {
-    int targetStatus, oldStatus;
+    int oldStatus, oldTargetStatus, newTargetStatus;
     DataLoaderStatusListener listener;
     {
         std::unique_lock lock(mMutex);
@@ -2730,22 +2732,31 @@
         }
 
         oldStatus = mCurrentStatus;
-        targetStatus = mTargetStatus;
+        oldTargetStatus = mTargetStatus;
         listener = mStatusListener;
 
         // Change the status.
         mCurrentStatus = newStatus;
         mCurrentStatusTs = mService.mClock->now();
 
-        if (mCurrentStatus == IDataLoaderStatusListener::DATA_LOADER_UNAVAILABLE ||
-            mCurrentStatus == IDataLoaderStatusListener::DATA_LOADER_UNRECOVERABLE) {
-            // For unavailable, unbind from DataLoader to ensure proper re-commit.
-            setTargetStatusLocked(IDataLoaderStatusListener::DATA_LOADER_DESTROYED);
+        switch (mCurrentStatus) {
+            case IDataLoaderStatusListener::DATA_LOADER_UNAVAILABLE:
+                // Unavailable, retry.
+                setTargetStatusLocked(IDataLoaderStatusListener::DATA_LOADER_STARTED);
+                break;
+            case IDataLoaderStatusListener::DATA_LOADER_UNRECOVERABLE:
+                // Unrecoverable, just unbind.
+                setTargetStatusLocked(IDataLoaderStatusListener::DATA_LOADER_DESTROYED);
+                break;
+            default:
+                break;
         }
+
+        newTargetStatus = mTargetStatus;
     }
 
     LOG(DEBUG) << "Current status update for DataLoader " << id() << ": " << oldStatus << " -> "
-               << newStatus << " (target " << targetStatus << ")";
+               << newStatus << " (target " << oldTargetStatus << " -> " << newTargetStatus << ")";
 
     if (listener) {
         listener->onStatusChanged(id(), newStatus);
diff --git a/services/incremental/test/IncrementalServiceTest.cpp b/services/incremental/test/IncrementalServiceTest.cpp
index 6a3d953..cdf2e89 100644
--- a/services/incremental/test/IncrementalServiceTest.cpp
+++ b/services/incremental/test/IncrementalServiceTest.cpp
@@ -126,7 +126,7 @@
 class MockDataLoader : public IDataLoader {
 public:
     MockDataLoader() {
-        ON_CALL(*this, create(_, _, _, _)).WillByDefault(Invoke(this, &MockDataLoader::createOk));
+        initializeCreateOk();
         ON_CALL(*this, start(_)).WillByDefault(Invoke(this, &MockDataLoader::startOk));
         ON_CALL(*this, stop(_)).WillByDefault(Invoke(this, &MockDataLoader::stopOk));
         ON_CALL(*this, destroy(_)).WillByDefault(Invoke(this, &MockDataLoader::destroyOk));
@@ -145,6 +145,10 @@
                  binder::Status(int32_t id, const std::vector<InstallationFileParcel>& addedFiles,
                                 const std::vector<std::string>& removedFiles));
 
+    void initializeCreateOk() {
+        ON_CALL(*this, create(_, _, _, _)).WillByDefault(Invoke(this, &MockDataLoader::createOk));
+    }
+
     void initializeCreateOkNoStatus() {
         ON_CALL(*this, create(_, _, _, _))
                 .WillByDefault(Invoke(this, &MockDataLoader::createOkNoStatus));
@@ -275,6 +279,14 @@
         }
         return binder::Status::ok();
     }
+    binder::Status bindToDataLoaderOkWithNoDelay(int32_t mountId,
+                                                 const DataLoaderParamsParcel& params,
+                                                 int bindDelayMs,
+                                                 const sp<IDataLoaderStatusListener>& listener,
+                                                 bool* _aidl_return) {
+        CHECK(bindDelayMs == 0) << bindDelayMs;
+        return bindToDataLoaderOk(mountId, params, bindDelayMs, listener, _aidl_return);
+    }
     binder::Status bindToDataLoaderOkWith1sDelay(int32_t mountId,
                                                  const DataLoaderParamsParcel& params,
                                                  int bindDelayMs,
@@ -338,14 +350,13 @@
         mListener->onStatusChanged(mId, IDataLoaderStatusListener::DATA_LOADER_UNRECOVERABLE);
     }
     binder::Status unbindFromDataLoaderOk(int32_t id) {
+        mBindDelayMs = -1;
         if (mDataLoader) {
             if (auto status = mDataLoader->destroy(id); !status.isOk()) {
                 return status;
             }
             mDataLoader = nullptr;
-        }
-        mBindDelayMs = -1;
-        if (mListener) {
+        } else if (mListener) {
             mListener->onStatusChanged(id, IDataLoaderStatusListener::DATA_LOADER_DESTROYED);
         }
         return binder::Status::ok();
@@ -1156,12 +1167,81 @@
     ASSERT_GE(storageId, 0);
     ASSERT_TRUE(mIncrementalService->startLoading(storageId, std::move(mDataLoaderParcel), {}, {},
                                                   {}, {}));
-    mDataLoaderManager->setDataLoaderStatusUnavailable();
+    mDataLoaderManager->setDataLoaderStatusUnrecoverable();
+
+    // Timed callback present.
+    ASSERT_EQ(storageId, mTimedQueue->mId);
+    ASSERT_GE(mTimedQueue->mAfter, 10ms);
+    auto timedCallback = mTimedQueue->mWhat;
+    mTimedQueue->clearJob(storageId);
+
+    // First callback call to propagate unrecoverable.
+    timedCallback();
+
+    // And second call to trigger recreation.
     ASSERT_NE(nullptr, mLooper->mCallback);
     ASSERT_NE(nullptr, mLooper->mCallbackData);
     mLooper->mCallback(-1, -1, mLooper->mCallbackData);
 }
 
+TEST_F(IncrementalServiceTest, testStartDataLoaderUnavailable) {
+    mIncFs->openMountSuccess();
+    mDataLoader->initializeCreateOkNoStatus();
+
+    EXPECT_CALL(*mDataLoaderManager, bindToDataLoader(_, _, _, _, _)).Times(3);
+    EXPECT_CALL(*mDataLoaderManager, unbindFromDataLoader(_)).Times(3);
+    EXPECT_CALL(*mDataLoader, create(_, _, _, _)).Times(3);
+    EXPECT_CALL(*mDataLoader, start(_)).Times(1);
+    EXPECT_CALL(*mDataLoader, destroy(_)).Times(2);
+    EXPECT_CALL(*mVold, unmountIncFs(_)).Times(2);
+    EXPECT_CALL(*mLooper, addFd(MockIncFs::kPendingReadsFd, _, _, _, _)).Times(1);
+    EXPECT_CALL(*mLooper, removeFd(MockIncFs::kPendingReadsFd)).Times(1);
+    TemporaryDir tempDir;
+    int storageId =
+            mIncrementalService->createStorage(tempDir.path, mDataLoaderParcel,
+                                               IncrementalService::CreateOptions::CreateNew);
+    ASSERT_GE(storageId, 0);
+
+    ON_CALL(*mDataLoaderManager, bindToDataLoader(_, _, _, _, _))
+            .WillByDefault(Invoke(mDataLoaderManager,
+                                  &MockDataLoaderManager::bindToDataLoaderOkWithNoDelay));
+
+    ASSERT_TRUE(mIncrementalService->startLoading(storageId, std::move(mDataLoaderParcel), {}, {},
+                                                  {}, {}));
+
+    // Unavailable.
+    mDataLoaderManager->setDataLoaderStatusUnavailable();
+
+    // Timed callback present.
+    ASSERT_EQ(storageId, mTimedQueue->mId);
+    ASSERT_GE(mTimedQueue->mAfter, 10ms);
+    auto timedCallback = mTimedQueue->mWhat;
+    mTimedQueue->clearJob(storageId);
+
+    // Propagating unavailable and expecting it to trigger rebind with 1s retry delay.
+    ON_CALL(*mDataLoaderManager, bindToDataLoader(_, _, _, _, _))
+            .WillByDefault(Invoke(mDataLoaderManager,
+                                  &MockDataLoaderManager::bindToDataLoaderOkWith1sDelay));
+    timedCallback();
+
+    // Unavailable #2.
+    mDataLoaderManager->setDataLoaderStatusUnavailable();
+
+    // Timed callback present.
+    ASSERT_EQ(storageId, mTimedQueue->mId);
+    ASSERT_GE(mTimedQueue->mAfter, 10ms);
+    timedCallback = mTimedQueue->mWhat;
+    mTimedQueue->clearJob(storageId);
+
+    // Propagating unavailable and expecting it to trigger rebind with 10s retry delay.
+    // This time succeed.
+    mDataLoader->initializeCreateOk();
+    ON_CALL(*mDataLoaderManager, bindToDataLoader(_, _, _, _, _))
+            .WillByDefault(Invoke(mDataLoaderManager,
+                                  &MockDataLoaderManager::bindToDataLoaderOkWith10sDelay));
+    timedCallback();
+}
+
 TEST_F(IncrementalServiceTest, testStartDataLoaderUnhealthyStorage) {
     mIncFs->openMountSuccess();