[incremental/pm] register progress listener

Incremental Serivce periodically polls loading progress and sends to
Package Manager Service. Package Manager provides APIs for other
interested parties to listen to the loading progress.

BUG: 165841827
Test: unit test
Change-Id: I44b9e17c2240b9efe53bc09fc728b6671f1f7dfe
diff --git a/Android.bp b/Android.bp
index eacf57c..c6f9362 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1029,6 +1029,7 @@
     name: "incremental_manager_aidl",
     srcs: [
         "core/java/android/os/incremental/IIncrementalService.aidl",
+        "core/java/android/os/incremental/IStorageLoadingProgressListener.aidl",
         "core/java/android/os/incremental/IncrementalNewFileParams.aidl",
         "core/java/android/os/incremental/IStorageHealthListener.aidl",
         "core/java/android/os/incremental/StorageHealthCheckParams.aidl",
diff --git a/core/java/android/content/pm/IPackageLoadingProgressCallback.aidl b/core/java/android/content/pm/IPackageLoadingProgressCallback.aidl
new file mode 100644
index 0000000..8adfb7a
--- /dev/null
+++ b/core/java/android/content/pm/IPackageLoadingProgressCallback.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2020 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 android.content.pm;
+
+/**
+ * Callbacks for Package Manager to report package loading progress to listeners.
+ * @hide
+ */
+oneway interface IPackageLoadingProgressCallback {
+    void onPackageLoadingProgressChanged(float progress);
+}
\ No newline at end of file
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 1f8cee2..244cfe1 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -31,6 +31,7 @@
 import android.content.pm.IPackageDeleteObserver;
 import android.content.pm.IPackageDeleteObserver2;
 import android.content.pm.IPackageDataObserver;
+import android.content.pm.IPackageLoadingProgressCallback;
 import android.content.pm.IPackageMoveObserver;
 import android.content.pm.IPackageStatsObserver;
 import android.content.pm.IntentFilterVerificationInfo;
diff --git a/core/java/android/os/incremental/IIncrementalService.aidl b/core/java/android/os/incremental/IIncrementalService.aidl
index f351c7d..ad9a162 100644
--- a/core/java/android/os/incremental/IIncrementalService.aidl
+++ b/core/java/android/os/incremental/IIncrementalService.aidl
@@ -19,6 +19,7 @@
 import android.content.pm.DataLoaderParamsParcel;
 import android.content.pm.IDataLoaderStatusListener;
 import android.os.incremental.IncrementalNewFileParams;
+import android.os.incremental.IStorageLoadingProgressListener;
 import android.os.incremental.IStorageHealthListener;
 import android.os.incremental.StorageHealthCheckParams;
 
@@ -133,4 +134,14 @@
      * Waits until all native library extraction is done for the storage
      */
     boolean waitForNativeBinariesExtraction(int storageId);
+
+    /**
+     * Register to start listening for loading progress change for a storage.
+     */
+    boolean registerLoadingProgressListener(int storageId, IStorageLoadingProgressListener listener);
+
+    /**
+     * Stop listening for the loading progress change for a storage.
+     */
+    boolean unregisterLoadingProgressListener(int storageId);
 }
diff --git a/core/java/android/os/incremental/IStorageLoadingProgressListener.aidl b/core/java/android/os/incremental/IStorageLoadingProgressListener.aidl
new file mode 100644
index 0000000..efb51fd
--- /dev/null
+++ b/core/java/android/os/incremental/IStorageLoadingProgressListener.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2020 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 android.os.incremental;
+
+/**
+ * Callbacks for Incremental Service to report storage loading progress to Package Manager Service.
+ * @hide
+ */
+oneway interface IStorageLoadingProgressListener {
+    void onStorageLoadingProgressChanged(int storageId, float progress);
+}
diff --git a/core/java/android/os/incremental/IncrementalManager.java b/core/java/android/os/incremental/IncrementalManager.java
index c7f50c9..768ef97 100644
--- a/core/java/android/os/incremental/IncrementalManager.java
+++ b/core/java/android/os/incremental/IncrementalManager.java
@@ -23,6 +23,8 @@
 import android.content.Context;
 import android.content.pm.DataLoaderParams;
 import android.content.pm.IDataLoaderStatusListener;
+import android.content.pm.IPackageLoadingProgressCallback;
+import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.util.SparseArray;
 
@@ -76,28 +78,15 @@
     }
 
     private final @Nullable IIncrementalService mService;
-    @GuardedBy("mStorages")
-    private final SparseArray<IncrementalStorage> mStorages = new SparseArray<>();
+
+    private final LoadingProgressCallbacks mLoadingProgressCallbacks =
+            new LoadingProgressCallbacks();
 
     public IncrementalManager(IIncrementalService service) {
         mService = service;
     }
 
     /**
-     * Returns a storage object given a storage ID.
-     *
-     * @param storageId The storage ID to identify the storage object.
-     * @return IncrementalStorage object corresponding to storage ID.
-     */
-    // TODO(b/136132412): remove this
-    @Nullable
-    public IncrementalStorage getStorage(int storageId) {
-        synchronized (mStorages) {
-            return mStorages.get(storageId);
-        }
-    }
-
-    /**
      * Opens or create an Incremental File System mounted directory and returns an
      * IncrementalStorage object.
      *
@@ -123,9 +112,6 @@
                 return null;
             }
             final IncrementalStorage storage = new IncrementalStorage(mService, id);
-            synchronized (mStorages) {
-                mStorages.put(id, storage);
-            }
             if (autoStartDataLoader) {
                 storage.startLoading();
             }
@@ -150,9 +136,6 @@
                 return null;
             }
             final IncrementalStorage storage = new IncrementalStorage(mService, id);
-            synchronized (mStorages) {
-                mStorages.put(id, storage);
-            }
             return storage;
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -173,11 +156,7 @@
             if (id < 0) {
                 return null;
             }
-            final IncrementalStorage storage = new IncrementalStorage(mService, id);
-            synchronized (mStorages) {
-                mStorages.put(id, storage);
-            }
-            return storage;
+            return new IncrementalStorage(mService, id);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -263,25 +242,6 @@
     }
 
     /**
-     * Closes a storage specified by the absolute path. If the path is not Incremental, do nothing.
-     * Unbinds the target dir and deletes the corresponding storage instance.
-     */
-    public void closeStorage(@NonNull String path) {
-        try {
-            final int id = mService.openStorage(path);
-            if (id < 0) {
-                return;
-            }
-            mService.deleteStorage(id);
-            synchronized (mStorages) {
-                mStorages.remove(id);
-            }
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
      * Checks if Incremental feature is enabled on this device.
      */
     public static boolean isFeatureEnabled() {
@@ -311,6 +271,149 @@
         return nativeUnsafeGetFileSignature(path);
     }
 
+    /**
+     * Closes a storage specified by the absolute path. If the path is not Incremental, do nothing.
+     * Unbinds the target dir and deletes the corresponding storage instance.
+     * Deletes the package name and associated storage id from maps.
+     */
+    public void onPackageRemoved(@NonNull String codePath) {
+        try {
+            final IncrementalStorage storage = openStorage(codePath);
+            if (storage == null) {
+                return;
+            }
+            mLoadingProgressCallbacks.cleanUpCallbacks(storage);
+            mService.deleteStorage(storage.getId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Called when a new callback wants to listen to the loading progress of an installed package.
+     * Increment the count of callbacks associated to the corresponding storage.
+     * Only register storage listener if there hasn't been any existing callback on the storage yet.
+     * @param codePath Path of the installed package. This path is on an Incremental Storage.
+     * @param callback To report loading progress to.
+     * @return True if the package name and associated storage id are valid. False otherwise.
+     */
+    public boolean registerCallback(@NonNull String codePath,
+            @NonNull IPackageLoadingProgressCallback callback) {
+        final IncrementalStorage storage = openStorage(codePath);
+        if (storage == null) {
+            // storage does not exist, package not installed
+            return false;
+        }
+        return mLoadingProgressCallbacks.registerCallback(storage, callback);
+    }
+
+    /**
+     * Called when a callback wants to stop listen to the loading progress of an installed package.
+     * Decrease the count of the callbacks on the associated to the corresponding storage.
+     * If the count becomes zero, unregister the storage listener.
+     * @param codePath Path of the installed package
+     * @return True if the package name and associated storage id are valid. False otherwise.
+     */
+    public boolean unregisterCallback(@NonNull String codePath,
+            @NonNull IPackageLoadingProgressCallback callback) {
+        final IncrementalStorage storage = openStorage(codePath);
+        if (storage == null) {
+            // storage does not exist, package not installed
+            return false;
+        }
+        return mLoadingProgressCallbacks.unregisterCallback(storage, callback);
+    }
+
+    private static class LoadingProgressCallbacks extends IStorageLoadingProgressListener.Stub {
+        @GuardedBy("mCallbacks")
+        private final SparseArray<RemoteCallbackList<IPackageLoadingProgressCallback>> mCallbacks =
+                new SparseArray<>();
+
+        // TODO(b/165841827): disable callbacks when app state changes to fully loaded
+        public void cleanUpCallbacks(@NonNull IncrementalStorage storage) {
+            final int storageId = storage.getId();
+            final RemoteCallbackList<IPackageLoadingProgressCallback> callbacksForStorage;
+            synchronized (mCallbacks) {
+                callbacksForStorage = mCallbacks.removeReturnOld(storageId);
+            }
+            if (callbacksForStorage == null) {
+                return;
+            }
+            // Unregister all existing callbacks on this storage
+            callbacksForStorage.kill();
+            storage.unregisterLoadingProgressListener();
+        }
+
+        // TODO(b/165841827): handle reboot and app update
+        public boolean registerCallback(@NonNull IncrementalStorage storage,
+                @NonNull IPackageLoadingProgressCallback callback) {
+            final int storageId = storage.getId();
+            synchronized (mCallbacks) {
+                RemoteCallbackList<IPackageLoadingProgressCallback> callbacksForStorage =
+                        mCallbacks.get(storageId);
+                if (callbacksForStorage == null) {
+                    callbacksForStorage = new RemoteCallbackList<>();
+                    mCallbacks.put(storageId, callbacksForStorage);
+                }
+                // Registration in RemoteCallbackList needs to be done first, such that when events
+                // come from Incremental Service, the callback is already registered
+                callbacksForStorage.register(callback);
+                if (callbacksForStorage.getRegisteredCallbackCount() > 1) {
+                    // already listening for progress for this storage
+                    return true;
+                }
+            }
+            return storage.registerLoadingProgressListener(this);
+        }
+
+        public boolean unregisterCallback(@NonNull IncrementalStorage storage,
+                @NonNull IPackageLoadingProgressCallback callback) {
+            final int storageId = storage.getId();
+            final RemoteCallbackList<IPackageLoadingProgressCallback> callbacksForStorage;
+            synchronized (mCallbacks) {
+                callbacksForStorage = mCallbacks.get(storageId);
+                if (callbacksForStorage == null) {
+                    // no callback has ever been registered on this storage
+                    return false;
+                }
+                if (!callbacksForStorage.unregister(callback)) {
+                    // the callback was not registered
+                    return false;
+                }
+                if (callbacksForStorage.getRegisteredCallbackCount() > 0) {
+                    // other callbacks are still listening on this storage
+                    return true;
+                }
+                mCallbacks.delete(storageId);
+            }
+            // stop listening for this storage
+            return storage.unregisterLoadingProgressListener();
+        }
+
+        @Override
+        public void onStorageLoadingProgressChanged(int storageId, float progress) {
+            final RemoteCallbackList<IPackageLoadingProgressCallback> callbacksForStorage;
+            synchronized (mCallbacks) {
+                callbacksForStorage = mCallbacks.get(storageId);
+            }
+            if (callbacksForStorage == null) {
+                // no callback has ever been registered on this storage
+                return;
+            }
+            final int n = callbacksForStorage.beginBroadcast();
+            // RemoteCallbackList use ArrayMap internally and it's safe to iterate this way
+            for (int i = 0; i < n; i++) {
+                final IPackageLoadingProgressCallback callback =
+                        callbacksForStorage.getBroadcastItem(i);
+                try {
+                    callback.onPackageLoadingProgressChanged(progress);
+                } catch (RemoteException ignored) {
+                }
+            }
+            callbacksForStorage.finishBroadcast();
+        }
+    }
+
     /* Native methods */
     private static native boolean nativeIsEnabled();
     private static native boolean nativeIsIncrementalPath(@NonNull String path);
diff --git a/core/java/android/os/incremental/IncrementalStorage.java b/core/java/android/os/incremental/IncrementalStorage.java
index ed386f7..f835412 100644
--- a/core/java/android/os/incremental/IncrementalStorage.java
+++ b/core/java/android/os/incremental/IncrementalStorage.java
@@ -519,4 +519,29 @@
             return false;
         }
     }
+
+    /**
+     * Register to listen to loading progress of all the files on this storage.
+     * @param listener To report progress from Incremental Service to the caller.
+     */
+    public boolean registerLoadingProgressListener(IStorageLoadingProgressListener listener) {
+        try {
+            return mService.registerLoadingProgressListener(mId, listener);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+            return false;
+        }
+    }
+
+    /**
+     * Unregister to stop listening to storage loading progress.
+     */
+    public boolean unregisterLoadingProgressListener() {
+        try {
+            return mService.unregisterLoadingProgressListener(mId);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+            return false;
+        }
+    }
 }
diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java
index cf9324c..0c56d46 100644
--- a/services/core/java/android/content/pm/PackageManagerInternal.java
+++ b/services/core/java/android/content/pm/PackageManagerInternal.java
@@ -32,11 +32,16 @@
 import android.content.pm.PackageManager.ResolveInfoFlags;
 import android.content.pm.parsing.component.ParsedMainComponent;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.IBinder;
+import android.os.Looper;
 import android.os.PersistableBundle;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.SparseArray;
 
+import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.pm.PackageList;
 import com.android.server.pm.PackageSetting;
 import com.android.server.pm.parsing.pkg.AndroidPackage;
@@ -46,6 +51,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.Collection;
 import java.util.List;
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 /**
@@ -986,4 +992,72 @@
      * Returns {@code true} if the package is suspending any packages for the user.
      */
     public abstract boolean isSuspendingAnyPackages(String suspendingPackage, int userId);
+
+    /**
+     * Register to listen for loading progress of an installed package.
+     * @param packageName The name of the installed package
+     * @param callback To loading reporting progress
+     * @param userId The user under which to check.
+     * @return Whether the registration was successful. It can fail if the package has not been
+     *          installed yet.
+     */
+    public abstract boolean registerInstalledLoadingProgressCallback(@NonNull String packageName,
+            @NonNull InstalledLoadingProgressCallback callback, int userId);
+
+    /**
+     * Unregister to stop listening to loading progress of an installed package
+     * @param packageName The name of the installed package
+     * @param callback To unregister
+     * @return True if the callback is removed from registered callback list. False is the callback
+     *         does not exist on the registered callback list, which can happen if the callback has
+     *         already been unregistered.
+     */
+    public abstract boolean unregisterInstalledLoadingProgressCallback(@NonNull String packageName,
+            @NonNull InstalledLoadingProgressCallback callback);
+
+    /**
+     * Callback to listen for loading progress of a package installed on Incremental File System.
+     */
+    public abstract static class InstalledLoadingProgressCallback {
+        final LoadingProgressCallbackBinder mBinder = new LoadingProgressCallbackBinder();
+        final Executor mExecutor;
+        /**
+         * Default constructor that should always be called on subclass instantiation
+         * @param handler To dispatch callback events through. If null, the main thread
+         *                handler will be used.
+         */
+        public InstalledLoadingProgressCallback(@Nullable Handler handler) {
+            if (handler == null) {
+                handler = new Handler(Looper.getMainLooper());
+            }
+            mExecutor = new HandlerExecutor(handler);
+        }
+
+        /**
+         * Binder used by Package Manager Service to register as a callback
+         * @return the binder object of IPackageLoadingProgressCallback
+         */
+        public final @NonNull IBinder getBinder() {
+            return mBinder;
+        }
+
+        /**
+         * Report loading progress of an installed package.
+         *
+         * @param progress    Loading progress between [0, 1] for the registered package.
+         */
+        public abstract void onLoadingProgressChanged(float progress);
+
+        private class LoadingProgressCallbackBinder extends
+                android.content.pm.IPackageLoadingProgressCallback.Stub {
+            @Override
+            public void onPackageLoadingProgressChanged(float progress) {
+                mExecutor.execute(PooledLambda.obtainRunnable(
+                        InstalledLoadingProgressCallback::onLoadingProgressChanged,
+                        InstalledLoadingProgressCallback.this,
+                        progress).recycleOnUse());
+            }
+        }
+    }
+
 }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 5850dc0..3e4e88c 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -176,6 +176,7 @@
 import android.content.pm.IPackageDeleteObserver2;
 import android.content.pm.IPackageInstallObserver2;
 import android.content.pm.IPackageInstaller;
+import android.content.pm.IPackageLoadingProgressCallback;
 import android.content.pm.IPackageManager;
 import android.content.pm.IPackageManagerNative;
 import android.content.pm.IPackageMoveObserver;
@@ -15993,7 +15994,7 @@
 
             String codePath = codeFile.getAbsolutePath();
             if (mIncrementalManager != null && isIncrementalPath(codePath)) {
-                mIncrementalManager.closeStorage(codePath);
+                mIncrementalManager.onPackageRemoved(codePath);
             }
 
             removeCodePathLI(codeFile);
@@ -17125,10 +17126,11 @@
                             & PackageManagerService.SCAN_AS_INSTANT_APP) != 0);
             final AndroidPackage pkg = reconciledPkg.pkgSetting.pkg;
             final String packageName = pkg.getPackageName();
+            final String codePath = pkg.getPath();
             final boolean onIncremental = mIncrementalManager != null
-                    && isIncrementalPath(pkg.getPath());
+                    && isIncrementalPath(codePath);
             if (onIncremental) {
-                IncrementalStorage storage = mIncrementalManager.openStorage(pkg.getPath());
+                IncrementalStorage storage = mIncrementalManager.openStorage(codePath);
                 if (storage == null) {
                     throw new IllegalArgumentException(
                             "Install: null storage for incremental package " + packageName);
@@ -25359,6 +25361,56 @@
         public boolean isSuspendingAnyPackages(String suspendingPackage, int userId) {
             return PackageManagerService.this.isSuspendingAnyPackages(suspendingPackage, userId);
         }
+
+        @Override
+        public boolean registerInstalledLoadingProgressCallback(String packageName,
+                PackageManagerInternal.InstalledLoadingProgressCallback callback, int userId) {
+            final int callingUid = Binder.getCallingUid();
+            mPermissionManager.enforceCrossUserPermission(
+                    callingUid, userId, true, false,
+                    "registerLoadingProgressCallback");
+            final PackageSetting ps;
+            synchronized (mLock) {
+                ps = mSettings.mPackages.get(packageName);
+                if (ps == null) {
+                    Slog.w(TAG, "Failed registering loading progress callback. Package "
+                            + packageName + " is not installed");
+                    return false;
+                }
+                if (shouldFilterApplicationLocked(ps, callingUid, userId)) {
+                    Slog.w(TAG, "Failed registering loading progress callback. Package "
+                            + packageName + " is not visible to the calling app");
+                    return false;
+                }
+                // TODO(b/165841827): return false if package is fully loaded
+            }
+            if (mIncrementalManager == null) {
+                Slog.w(TAG,
+                        "Failed registering loading progress callback. Incremental is not enabled");
+                return false;
+            }
+            return mIncrementalManager.registerCallback(ps.getPathString(),
+                    (IPackageLoadingProgressCallback) callback.getBinder());
+        }
+
+        @Override
+        public boolean unregisterInstalledLoadingProgressCallback(String packageName,
+                PackageManagerInternal.InstalledLoadingProgressCallback callback) {
+            final PackageSetting ps;
+            synchronized (mLock) {
+                ps = mSettings.mPackages.get(packageName);
+                if (ps == null) {
+                    Slog.w(TAG, "Failed unregistering loading progress callback. Package "
+                            + packageName + " is not installed");
+                    return false;
+                }
+            }
+            if (mIncrementalManager == null) {
+                return false;
+            }
+            return mIncrementalManager.unregisterCallback(ps.getPathString(),
+                    (IPackageLoadingProgressCallback) callback.getBinder());
+        }
     }
 
     @GuardedBy("mLock")
diff --git a/services/incremental/BinderIncrementalService.cpp b/services/incremental/BinderIncrementalService.cpp
index 87ae4d7..bf3a896 100644
--- a/services/incremental/BinderIncrementalService.cpp
+++ b/services/incremental/BinderIncrementalService.cpp
@@ -301,6 +301,20 @@
     return ok();
 }
 
+binder::Status BinderIncrementalService::registerLoadingProgressListener(
+        int32_t storageId,
+        const ::android::sp<::android::os::incremental::IStorageLoadingProgressListener>&
+                progressListener,
+        bool* _aidl_return) {
+    *_aidl_return = mImpl.registerLoadingProgressListener(storageId, progressListener);
+    return ok();
+}
+binder::Status BinderIncrementalService::unregisterLoadingProgressListener(int32_t storageId,
+                                                                           bool* _aidl_return) {
+    *_aidl_return = mImpl.unregisterLoadingProgressListener(storageId);
+    return ok();
+}
+
 } // namespace android::os::incremental
 
 jlong Incremental_IncrementalService_Start(JNIEnv* env) {
diff --git a/services/incremental/BinderIncrementalService.h b/services/incremental/BinderIncrementalService.h
index 8478142..1238498 100644
--- a/services/incremental/BinderIncrementalService.h
+++ b/services/incremental/BinderIncrementalService.h
@@ -81,6 +81,12 @@
                                            const std::string& abi, bool extractNativeLibs,
                                            bool* _aidl_return) final;
     binder::Status waitForNativeBinariesExtraction(int storageId, bool* _aidl_return) final;
+    binder::Status registerLoadingProgressListener(
+            int32_t storageId,
+            const ::android::sp<::android::os::incremental::IStorageLoadingProgressListener>&
+                    progressListener,
+            bool* _aidl_return) final;
+    binder::Status unregisterLoadingProgressListener(int32_t storageId, bool* _aidl_return) final;
 
 private:
     android::incremental::IncrementalService mImpl;
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index 447ee55..10a508b 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -275,6 +275,7 @@
         mJni(sm.getJni()),
         mLooper(sm.getLooper()),
         mTimedQueue(sm.getTimedQueue()),
+        mProgressUpdateJobQueue(sm.getProgressUpdateJobQueue()),
         mFs(sm.getFs()),
         mIncrementalDir(rootDir) {
     CHECK(mVold) << "Vold service is unavailable";
@@ -283,6 +284,7 @@
     CHECK(mJni) << "JNI is unavailable";
     CHECK(mLooper) << "Looper is unavailable";
     CHECK(mTimedQueue) << "TimedQueue is unavailable";
+    CHECK(mProgressUpdateJobQueue) << "mProgressUpdateJobQueue is unavailable";
     CHECK(mFs) << "Fs is unavailable";
 
     mJobQueue.reserve(16);
@@ -308,6 +310,7 @@
     mJobProcessor.join();
     mCmdLooperThread.join();
     mTimedQueue->stop();
+    mProgressUpdateJobQueue->stop();
     // Ensure that mounts are destroyed while the service is still valid.
     mBindsByPath.clear();
     mMounts.clear();
@@ -1744,6 +1747,35 @@
     return (float)filledBlocks / (float)totalBlocks;
 }
 
+bool IncrementalService::updateLoadingProgress(
+        StorageId storage, const StorageLoadingProgressListener& progressListener) {
+    const auto progress = getLoadingProgress(storage);
+    if (progress < 0) {
+        // Failed to get progress from incfs, abort.
+        return false;
+    }
+    progressListener->onStorageLoadingProgressChanged(storage, progress);
+    if (progress > 1 - 0.001f) {
+        // Stop updating progress once it is fully loaded
+        return true;
+    }
+    static constexpr auto kProgressUpdateInterval = 1000ms;
+    addTimedJob(*mProgressUpdateJobQueue, storage, kProgressUpdateInterval /* repeat after 1s */,
+                [storage, progressListener, this]() {
+                    updateLoadingProgress(storage, progressListener);
+                });
+    return true;
+}
+
+bool IncrementalService::registerLoadingProgressListener(
+        StorageId storage, const StorageLoadingProgressListener& progressListener) {
+    return updateLoadingProgress(storage, progressListener);
+}
+
+bool IncrementalService::unregisterLoadingProgressListener(StorageId storage) {
+    return removeTimedJobs(*mProgressUpdateJobQueue, storage);
+}
+
 bool IncrementalService::perfLoggingEnabled() {
     static const bool enabled = base::GetBoolProperty("incremental.perflogging", false);
     return enabled;
@@ -1826,18 +1858,21 @@
     }
 }
 
-void IncrementalService::addTimedJob(MountId id, Milliseconds after, Job what) {
+bool IncrementalService::addTimedJob(TimedQueueWrapper& timedQueue, MountId id, Milliseconds after,
+                                     Job what) {
     if (id == kInvalidStorageId) {
-        return;
+        return false;
     }
-    mTimedQueue->addJob(id, after, std::move(what));
+    timedQueue.addJob(id, after, std::move(what));
+    return true;
 }
 
-void IncrementalService::removeTimedJobs(MountId id) {
+bool IncrementalService::removeTimedJobs(TimedQueueWrapper& timedQueue, MountId id) {
     if (id == kInvalidStorageId) {
-        return;
+        return false;
     }
-    mTimedQueue->removeJobs(id);
+    timedQueue.removeJobs(id);
+    return true;
 }
 
 IncrementalService::DataLoaderStub::DataLoaderStub(IncrementalService& service, MountId id,
@@ -1879,7 +1914,7 @@
         mHealthPath.clear();
         unregisterFromPendingReads();
         resetHealthControl();
-        mService.removeTimedJobs(mId);
+        mService.removeTimedJobs(*mService.mTimedQueue, mId);
     }
 
     requestDestroy();
@@ -2169,7 +2204,8 @@
         }
         LOG(DEBUG) << id() << ": updateHealthStatus in " << double(checkBackAfter.count()) / 1000.0
                    << "secs";
-        mService.addTimedJob(id(), checkBackAfter, [this]() { updateHealthStatus(); });
+        mService.addTimedJob(*mService.mTimedQueue, id(), checkBackAfter,
+                             [this]() { updateHealthStatus(); });
     }
 
     // With kTolerance we are expecting these to execute before the next update.
diff --git a/services/incremental/IncrementalService.h b/services/incremental/IncrementalService.h
index 267458d..a49e0f3 100644
--- a/services/incremental/IncrementalService.h
+++ b/services/incremental/IncrementalService.h
@@ -22,6 +22,7 @@
 #include <android/content/pm/IDataLoaderStatusListener.h>
 #include <android/os/incremental/BnIncrementalServiceConnector.h>
 #include <android/os/incremental/BnStorageHealthListener.h>
+#include <android/os/incremental/BnStorageLoadingProgressListener.h>
 #include <android/os/incremental/StorageHealthCheckParams.h>
 #include <binder/IAppOpsCallback.h>
 #include <utils/String16.h>
@@ -65,6 +66,8 @@
 using StorageHealthCheckParams = ::android::os::incremental::StorageHealthCheckParams;
 using IStorageHealthListener = ::android::os::incremental::IStorageHealthListener;
 using StorageHealthListener = ::android::sp<IStorageHealthListener>;
+using IStorageLoadingProgressListener = ::android::os::incremental::IStorageLoadingProgressListener;
+using StorageLoadingProgressListener = ::android::sp<IStorageLoadingProgressListener>;
 
 class IncrementalService final {
 public:
@@ -134,6 +137,9 @@
 
     int isFileFullyLoaded(StorageId storage, const std::string& path) const;
     float getLoadingProgress(StorageId storage) const;
+    bool registerLoadingProgressListener(StorageId storage,
+                                         const StorageLoadingProgressListener& progressListener);
+    bool unregisterLoadingProgressListener(StorageId storage);
 
     RawMetadata getMetadata(StorageId storage, std::string_view path) const;
     RawMetadata getMetadata(StorageId storage, FileId node) const;
@@ -354,8 +360,10 @@
 
     void runCmdLooper();
 
-    void addTimedJob(MountId id, Milliseconds after, Job what);
-    void removeTimedJobs(MountId id);
+    bool addTimedJob(TimedQueueWrapper& timedQueue, MountId id, Milliseconds after, Job what);
+    bool removeTimedJobs(TimedQueueWrapper& timedQueue, MountId id);
+    bool updateLoadingProgress(int32_t storageId,
+                               const StorageLoadingProgressListener& progressListener);
 
 private:
     const std::unique_ptr<VoldServiceWrapper> mVold;
@@ -365,6 +373,7 @@
     const std::unique_ptr<JniWrapper> mJni;
     const std::unique_ptr<LooperWrapper> mLooper;
     const std::unique_ptr<TimedQueueWrapper> mTimedQueue;
+    const std::unique_ptr<TimedQueueWrapper> mProgressUpdateJobQueue;
     const std::unique_ptr<FsWrapper> mFs;
     const std::string mIncrementalDir;
 
diff --git a/services/incremental/ServiceWrappers.cpp b/services/incremental/ServiceWrappers.cpp
index f6d89c5..144c466 100644
--- a/services/incremental/ServiceWrappers.cpp
+++ b/services/incremental/ServiceWrappers.cpp
@@ -357,6 +357,10 @@
     return std::make_unique<RealTimedQueueWrapper>(mJvm);
 }
 
+std::unique_ptr<TimedQueueWrapper> RealServiceManager::getProgressUpdateJobQueue() {
+    return std::make_unique<RealTimedQueueWrapper>(mJvm);
+}
+
 std::unique_ptr<FsWrapper> RealServiceManager::getFs() {
     return std::make_unique<RealFsWrapper>();
 }
diff --git a/services/incremental/ServiceWrappers.h b/services/incremental/ServiceWrappers.h
index 6376d86..4815caf 100644
--- a/services/incremental/ServiceWrappers.h
+++ b/services/incremental/ServiceWrappers.h
@@ -154,6 +154,7 @@
     virtual std::unique_ptr<JniWrapper> getJni() = 0;
     virtual std::unique_ptr<LooperWrapper> getLooper() = 0;
     virtual std::unique_ptr<TimedQueueWrapper> getTimedQueue() = 0;
+    virtual std::unique_ptr<TimedQueueWrapper> getProgressUpdateJobQueue() = 0;
     virtual std::unique_ptr<FsWrapper> getFs() = 0;
 };
 
@@ -170,6 +171,7 @@
     std::unique_ptr<JniWrapper> getJni() final;
     std::unique_ptr<LooperWrapper> getLooper() final;
     std::unique_ptr<TimedQueueWrapper> getTimedQueue() final;
+    std::unique_ptr<TimedQueueWrapper> getProgressUpdateJobQueue() final;
     std::unique_ptr<FsWrapper> getFs() final;
 
 private:
diff --git a/services/incremental/test/IncrementalServiceTest.cpp b/services/incremental/test/IncrementalServiceTest.cpp
index a290a17..aec9fa1 100644
--- a/services/incremental/test/IncrementalServiceTest.cpp
+++ b/services/incremental/test/IncrementalServiceTest.cpp
@@ -504,6 +504,14 @@
     int32_t mStatus = -1;
 };
 
+class MockStorageLoadingProgressListener : public IStorageLoadingProgressListener {
+public:
+    MockStorageLoadingProgressListener() = default;
+    MOCK_METHOD2(onStorageLoadingProgressChanged,
+                 binder::Status(int32_t storageId, float progress));
+    MOCK_METHOD0(onAsBinder, IBinder*());
+};
+
 class MockServiceManager : public ServiceManagerWrapper {
 public:
     MockServiceManager(std::unique_ptr<MockVoldService> vold,
@@ -513,6 +521,7 @@
                        std::unique_ptr<MockJniWrapper> jni,
                        std::unique_ptr<MockLooperWrapper> looper,
                        std::unique_ptr<MockTimedQueueWrapper> timedQueue,
+                       std::unique_ptr<MockTimedQueueWrapper> progressUpdateJobQueue,
                        std::unique_ptr<MockFsWrapper> fs)
           : mVold(std::move(vold)),
             mDataLoaderManager(std::move(dataLoaderManager)),
@@ -521,6 +530,7 @@
             mJni(std::move(jni)),
             mLooper(std::move(looper)),
             mTimedQueue(std::move(timedQueue)),
+            mProgressUpdateJobQueue(std::move(progressUpdateJobQueue)),
             mFs(std::move(fs)) {}
     std::unique_ptr<VoldServiceWrapper> getVoldService() final { return std::move(mVold); }
     std::unique_ptr<DataLoaderManagerWrapper> getDataLoaderManager() final {
@@ -533,6 +543,9 @@
     std::unique_ptr<JniWrapper> getJni() final { return std::move(mJni); }
     std::unique_ptr<LooperWrapper> getLooper() final { return std::move(mLooper); }
     std::unique_ptr<TimedQueueWrapper> getTimedQueue() final { return std::move(mTimedQueue); }
+    std::unique_ptr<TimedQueueWrapper> getProgressUpdateJobQueue() final {
+        return std::move(mProgressUpdateJobQueue);
+    }
     std::unique_ptr<FsWrapper> getFs() final { return std::move(mFs); }
 
 private:
@@ -543,6 +556,7 @@
     std::unique_ptr<MockJniWrapper> mJni;
     std::unique_ptr<MockLooperWrapper> mLooper;
     std::unique_ptr<MockTimedQueueWrapper> mTimedQueue;
+    std::unique_ptr<MockTimedQueueWrapper> mProgressUpdateJobQueue;
     std::unique_ptr<MockFsWrapper> mFs;
 };
 
@@ -567,19 +581,19 @@
         mLooper = looper.get();
         auto timedQueue = std::make_unique<NiceMock<MockTimedQueueWrapper>>();
         mTimedQueue = timedQueue.get();
+        auto progressUpdateJobQueue = std::make_unique<NiceMock<MockTimedQueueWrapper>>();
+        mProgressUpdateJobQueue = progressUpdateJobQueue.get();
         auto fs = std::make_unique<NiceMock<MockFsWrapper>>();
         mFs = fs.get();
-        mIncrementalService =
-                std::make_unique<IncrementalService>(MockServiceManager(std::move(vold),
-                                                                        std::move(
-                                                                                dataloaderManager),
-                                                                        std::move(incFs),
-                                                                        std::move(appOps),
-                                                                        std::move(jni),
-                                                                        std::move(looper),
-                                                                        std::move(timedQueue),
-                                                                        std::move(fs)),
-                                                     mRootDir.path);
+        mIncrementalService = std::make_unique<
+                IncrementalService>(MockServiceManager(std::move(vold),
+                                                       std::move(dataloaderManager),
+                                                       std::move(incFs), std::move(appOps),
+                                                       std::move(jni), std::move(looper),
+                                                       std::move(timedQueue),
+                                                       std::move(progressUpdateJobQueue),
+                                                       std::move(fs)),
+                                    mRootDir.path);
         mDataLoaderParcel.packageName = "com.test";
         mDataLoaderParcel.arguments = "uri";
         mDataLoaderManager->unbindFromDataLoaderSuccess();
@@ -624,6 +638,7 @@
     NiceMock<MockJniWrapper>* mJni = nullptr;
     NiceMock<MockLooperWrapper>* mLooper = nullptr;
     NiceMock<MockTimedQueueWrapper>* mTimedQueue = nullptr;
+    NiceMock<MockTimedQueueWrapper>* mProgressUpdateJobQueue = nullptr;
     NiceMock<MockFsWrapper>* mFs = nullptr;
     NiceMock<MockDataLoader>* mDataLoader = nullptr;
     std::unique_ptr<IncrementalService> mIncrementalService;
@@ -1166,4 +1181,44 @@
     EXPECT_CALL(*mIncFs, countFilledBlocks(_, _)).Times(3);
     ASSERT_EQ(0.5, mIncrementalService->getLoadingProgress(storageId));
 }
+
+TEST_F(IncrementalServiceTest, testRegisterLoadingProgressListenerSuccess) {
+    mIncFs->countFilledBlocksSuccess();
+    mFs->hasFiles();
+
+    TemporaryDir tempDir;
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
+    sp<NiceMock<MockStorageLoadingProgressListener>> listener{
+            new NiceMock<MockStorageLoadingProgressListener>};
+    NiceMock<MockStorageLoadingProgressListener>* listenerMock = listener.get();
+    EXPECT_CALL(*listenerMock, onStorageLoadingProgressChanged(_, _)).Times(2);
+    EXPECT_CALL(*mProgressUpdateJobQueue, addJob(_, _, _)).Times(2);
+    mIncrementalService->registerLoadingProgressListener(storageId, listener);
+    // Timed callback present.
+    ASSERT_EQ(storageId, mProgressUpdateJobQueue->mId);
+    ASSERT_EQ(mProgressUpdateJobQueue->mAfter, 1000ms);
+    auto timedCallback = mProgressUpdateJobQueue->mWhat;
+    timedCallback();
+    ASSERT_EQ(storageId, mProgressUpdateJobQueue->mId);
+    ASSERT_EQ(mProgressUpdateJobQueue->mAfter, 1000ms);
+    mIncrementalService->unregisterLoadingProgressListener(storageId);
+    ASSERT_EQ(mProgressUpdateJobQueue->mAfter, Milliseconds{});
+}
+
+TEST_F(IncrementalServiceTest, testRegisterLoadingProgressListenerFailsToGetProgress) {
+    mIncFs->countFilledBlocksFails();
+    mFs->hasFiles();
+
+    TemporaryDir tempDir;
+    int storageId = mIncrementalService->createStorage(tempDir.path, std::move(mDataLoaderParcel),
+                                                       IncrementalService::CreateOptions::CreateNew,
+                                                       {}, {}, {});
+    sp<NiceMock<MockStorageLoadingProgressListener>> listener{
+            new NiceMock<MockStorageLoadingProgressListener>};
+    NiceMock<MockStorageLoadingProgressListener>* listenerMock = listener.get();
+    EXPECT_CALL(*listenerMock, onStorageLoadingProgressChanged(_, _)).Times(0);
+    mIncrementalService->registerLoadingProgressListener(storageId, listener);
+}
 } // namespace android::os::incremental