[incremental/pm] app states and transitions

Based on go/incremental-states-design with basic
setter/getters.

Defines IncrementalStates class which handles state transitions.

New (internal) Intent actions: PACKAGE_FULLY_LOADED, PACKAGE_STARTABLE,
PACKAGE_UNSTARTABLE.

BUG: 168043976
Test: unit tests
Change-Id: I7b0ec2dd9f028ee620a9307a1e71ddf12ea5a9af
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 35c7b96..9a9f165 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2732,6 +2732,54 @@
     public static final String ACTION_MY_PACKAGE_UNSUSPENDED = "android.intent.action.MY_PACKAGE_UNSUSPENDED";
 
     /**
+     * Broadcast Action: Sent to indicate that the package becomes startable.
+     * The intent will have the following extra values:
+     * <ul>
+     * <li> {@link #EXTRA_UID} containing the integer uid assigned to the package. </li>
+     * <li> {@link #EXTRA_PACKAGE_NAME} containing the package name. </li>
+     * </li>
+     * </ul>
+     *
+     * <p class="note">This is a protected intent that can only be sent by the system.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_PACKAGE_STARTABLE = "android.intent.action.PACKAGE_STARTABLE";
+
+    /**
+     * Broadcast Action: Sent to indicate that the package becomes unstartable.
+     * The intent will have the following extra values:
+     * <ul>
+     * <li> {@link #EXTRA_UID} containing the integer uid assigned to the package. </li>
+     * <li> {@link #EXTRA_PACKAGE_NAME} containing the package name. </li>
+     * <li> {@link #EXTRA_REASON} containing the integer indicating the reason for the state change,
+     * @see PackageManager.UnstartableReason
+     * </li>
+     * </ul>
+     *
+     * <p class="note">This is a protected intent that can only be sent by the system.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_PACKAGE_UNSTARTABLE =
+            "android.intent.action.PACKAGE_UNSTARTABLE";
+
+    /**
+     * Broadcast Action: Sent to indicate that the package is fully loaded.
+     * <ul>
+     * <li> {@link #EXTRA_UID} containing the integer uid assigned to the package. </li>
+     * <li> {@link #EXTRA_PACKAGE_NAME} containing the package name. </li>
+     * </li>
+     * </ul>
+     *
+     * <p class="note">This is a protected intent that can only be sent by the system.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_PACKAGE_FULLY_LOADED =
+            "android.intent.action.PACKAGE_FULLY_LOADED";
+
+    /**
      * Broadcast Action: A user ID has been removed from the system.  The user
      * ID number is stored in the extra data under {@link #EXTRA_UID}.
      *
diff --git a/core/java/android/content/pm/IDataLoaderStatusListener.aidl b/core/java/android/content/pm/IDataLoaderStatusListener.aidl
index efb00a0..745c39b 100644
--- a/core/java/android/content/pm/IDataLoaderStatusListener.aidl
+++ b/core/java/android/content/pm/IDataLoaderStatusListener.aidl
@@ -50,7 +50,30 @@
     *            fail and all retry limits are exceeded. */
     const int DATA_LOADER_UNRECOVERABLE = 8;
 
+    /** There are no known issues with the data stream. */
+    const int STREAM_HEALTHY = 0;
+
+    /** There are issues with the current transport layer (network, adb connection, etc.) that may
+     * recover automatically or could eventually require user intervention. */
+    const int STREAM_TRANSPORT_ERROR = 1;
+
+    /** Integrity failures in the data stream, this could be due to file corruption, decompression
+     * issues or similar. This indicates a likely unrecoverable error. */
+    const int STREAM_INTEGRITY_ERROR = 2;
+
+    /** There are issues with the source of the data, e.g., backend availability issues, account
+     * issues. This indicates a potentially recoverable error, but one that may take a long time to
+     * resolve. */
+    const int STREAM_SOURCE_ERROR = 3;
+
+    /** The device or app is low on storage and cannot complete the stream as a result.
+      * A subsequent page miss resulting in app failure will transition app to unstartable state. */
+    const int STREAM_STORAGE_ERROR = 4;
+
     /** Data loader status callback */
     void onStatusChanged(in int dataLoaderId, in int status);
+
+    /** Callback to report streaming health status of a specific data loader */
+    void reportStreamHealth(in int dataLoaderId, in int streamStatus);
 }
 
diff --git a/core/java/android/content/pm/IncrementalStatesInfo.aidl b/core/java/android/content/pm/IncrementalStatesInfo.aidl
new file mode 100644
index 0000000..b5aad12
--- /dev/null
+++ b/core/java/android/content/pm/IncrementalStatesInfo.aidl
@@ -0,0 +1,20 @@
+/*
+**
+** Copyright 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 com.android.server.pm;
+
+parcelable IncrementalStatesInfo;
\ No newline at end of file
diff --git a/core/java/android/content/pm/IncrementalStatesInfo.java b/core/java/android/content/pm/IncrementalStatesInfo.java
new file mode 100644
index 0000000..6e91c19
--- /dev/null
+++ b/core/java/android/content/pm/IncrementalStatesInfo.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Info about a package's states in Parcelable format.
+ * @hide
+ */
+public class IncrementalStatesInfo implements Parcelable {
+    private boolean mIsStartable;
+    private boolean mIsLoading;
+    private float mProgress;
+
+    public IncrementalStatesInfo(boolean isStartable, boolean isLoading, float progress) {
+        mIsStartable = isStartable;
+        mIsLoading = isLoading;
+        mProgress = progress;
+    }
+
+    private IncrementalStatesInfo(Parcel source) {
+        mIsStartable = source.readBoolean();
+        mIsLoading = source.readBoolean();
+        mProgress = source.readFloat();
+    }
+
+    public boolean isStartable() {
+        return mIsStartable;
+    }
+
+    public boolean isLoading() {
+        return mIsLoading;
+    }
+
+    public float getProgress() {
+        return mProgress;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeBoolean(mIsStartable);
+        dest.writeBoolean(mIsLoading);
+        dest.writeFloat(mProgress);
+    }
+
+    public static final @android.annotation.NonNull Creator<IncrementalStatesInfo> CREATOR =
+            new Creator<IncrementalStatesInfo>() {
+                public IncrementalStatesInfo createFromParcel(Parcel source) {
+                    return new IncrementalStatesInfo(source);
+                }
+                public IncrementalStatesInfo[] newArray(int size) {
+                    return new IncrementalStatesInfo[size];
+                }
+            };
+}
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 79e23b3..0e47b06 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -3738,6 +3738,39 @@
      */
     public static final int SYSTEM_APP_STATE_UNINSTALLED = 3;
 
+    /**
+     * Reasons for why a package is unstartable.
+     * @hide
+     */
+    @IntDef({UNSTARTABLE_REASON_UNKNOWN,
+            UNSTARTABLE_REASON_DATALOADER_TRANSPORT,
+            UNSTARTABLE_REASON_DATALOADER_STORAGE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface UnstartableReason {}
+
+    /**
+     * Unstartable state with no root cause specified. E.g., data loader seeing missing pages but
+     * unclear about the cause. This corresponds to a generic alert window shown to the user when
+     * the user attempts to launch the app.
+     * @hide
+     */
+    public static final int UNSTARTABLE_REASON_UNKNOWN = 0;
+
+    /**
+     * Unstartable state after hint from dataloader of issues with the transport layer.
+     * This corresponds to an alert window shown to the user indicating network errors.
+     * @hide
+     */
+    public static final int UNSTARTABLE_REASON_DATALOADER_TRANSPORT = 1;
+
+    /**
+     * Unstartable state after encountering storage limitations.
+     * This corresponds to an alert window indicating limited storage.
+     * @hide
+     */
+    public static final int UNSTARTABLE_REASON_DATALOADER_STORAGE = 2;
+
     /** {@hide} */
     public int getUserId() {
         return UserHandle.myUserId();
diff --git a/core/java/android/content/pm/PackageUserState.java b/core/java/android/content/pm/PackageUserState.java
index 327d1b8..3ed21b0 100644
--- a/core/java/android/content/pm/PackageUserState.java
+++ b/core/java/android/content/pm/PackageUserState.java
@@ -44,7 +44,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.CollectionUtils;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/proto/android/service/package.proto b/core/proto/android/service/package.proto
index 004b096..d289e00 100644
--- a/core/proto/android/service/package.proto
+++ b/core/proto/android/service/package.proto
@@ -124,6 +124,11 @@
         optional string originating_package_name = 2;
     }
 
+    message StatesProto {
+        optional bool is_startable = 1;
+        optional bool is_loading = 2;
+    }
+
     // Name of package. e.g. "com.android.providers.telephony".
     optional string name = 1;
     // UID for this package as assigned by Android OS.
@@ -145,4 +150,6 @@
     repeated UserInfoProto users = 9;
     // Where the request to install this package came from,
     optional InstallSourceProto install_source = 10;
+    // Whether the package is startable or is still loading
+    optional StatesProto states = 11;
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 723cceb..846ed87 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -42,6 +42,9 @@
     <protected-broadcast android:name="android.intent.action.PACKAGE_REMOVED" />
     <protected-broadcast android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
     <protected-broadcast android:name="android.intent.action.PACKAGE_CHANGED" />
+    <protected-broadcast android:name="android.intent.action.PACKAGE_STARTABLE" />
+    <protected-broadcast android:name="android.intent.action.PACKAGE_UNSTARTABLE" />
+    <protected-broadcast android:name="android.intent.action.PACKAGE_FULLY_LOADED" />
     <protected-broadcast android:name="android.intent.action.PACKAGE_ENABLE_ROLLBACK" />
     <protected-broadcast android:name="android.intent.action.CANCEL_ENABLE_ROLLBACK" />
     <protected-broadcast android:name="android.intent.action.ROLLBACK_COMMITTED" />
diff --git a/services/core/java/com/android/server/pm/IncrementalStates.java b/services/core/java/com/android/server/pm/IncrementalStates.java
new file mode 100644
index 0000000..dda5faf
--- /dev/null
+++ b/services/core/java/com/android/server/pm/IncrementalStates.java
@@ -0,0 +1,480 @@
+/*
+ * 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 com.android.server.pm;
+
+import android.content.pm.IDataLoaderStatusListener;
+import android.content.pm.IncrementalStatesInfo;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.incremental.IStorageHealthListener;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.function.pooled.PooledLambda;
+
+import java.util.function.BiConsumer;
+
+/**
+ * Manages state transitions of a package installed on Incremental File System. Currently manages:
+ * 1. startable state (whether a package is allowed to be launched), and
+ * 2. loading state (whether a package is still loading or has been fully loaded).
+ *
+ * The following events might change the states of a package:
+ * 1. Installation commit
+ * 2. Incremental storage health
+ * 3. Data loader stream health
+ * 4. Loading progress changes
+ *
+ * @hide
+ */
+public final class IncrementalStates {
+    private static final String TAG = "IncrementalStates";
+    private static final boolean DEBUG = false;
+    private final Handler mHandler = BackgroundThread.getHandler();
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private int mStreamStatus = IDataLoaderStatusListener.STREAM_HEALTHY;
+    @GuardedBy("mLock")
+    private int mStorageHealthStatus = IStorageHealthListener.HEALTH_STATUS_OK;
+    @GuardedBy("mLock")
+    private final LoadingState mLoadingState;
+    @GuardedBy("mLock")
+    private StartableState mStartableState;
+    @GuardedBy("mLock")
+    private Callback mCallback = null;
+    private final BiConsumer<Integer, Integer> mStatusConsumer;
+
+    public IncrementalStates() {
+        // By default the package is not startable and not fully loaded (i.e., is loading)
+        this(false, true);
+    }
+
+    public IncrementalStates(boolean isStartable, boolean isLoading) {
+        mStartableState = new StartableState(isStartable);
+        mLoadingState = new LoadingState(isLoading);
+        mStatusConsumer = new StatusConsumer();
+    }
+
+    /**
+     * Callback interface to report that the startable state of this package has changed.
+     */
+    public interface Callback {
+        /**
+         * Reports that the package is now unstartable and the unstartable reason.
+         */
+        void onPackageUnstartable(int reason);
+
+        /**
+         * Reports that the package is now startable.
+         */
+        void onPackageStartable();
+
+        /**
+         * Reports that package is fully loaded.
+         */
+        void onPackageFullyLoaded();
+    }
+
+    /**
+     * By calling this method, the caller indicates that package installation has just been
+     * committed. The package becomes startable. Set the initial loading state after the package
+     * is committed. Incremental packages are by-default loading; non-Incremental packages are not.
+     *
+     * @param isIncremental whether a package is installed on Incremental or not.
+     */
+    public void onCommit(boolean isIncremental) {
+        if (DEBUG) {
+            Slog.i(TAG, "received package commit event");
+        }
+        synchronized (mLock) {
+            if (!mStartableState.isStartable()) {
+                mStartableState.adoptNewStartableStateLocked(true);
+            }
+            if (mLoadingState.isLoading() != isIncremental) {
+                mLoadingState.adoptNewLoadingStateLocked(isIncremental);
+            }
+        }
+        mHandler.post(PooledLambda.obtainRunnable(
+                IncrementalStates::reportStartableState,
+                IncrementalStates.this).recycleOnUse());
+        if (!isIncremental) {
+            mHandler.post(PooledLambda.obtainRunnable(
+                    IncrementalStates::reportFullyLoaded,
+                    IncrementalStates.this).recycleOnUse());
+        }
+    }
+
+    private void reportStartableState() {
+        final Callback callback;
+        final boolean startable;
+        final int reason;
+        synchronized (mLock) {
+            callback = mCallback;
+            startable = mStartableState.isStartable();
+            reason = mStartableState.getUnstartableReason();
+        }
+        if (callback == null) {
+            return;
+        }
+        if (startable) {
+            callback.onPackageStartable();
+        } else {
+            callback.onPackageUnstartable(reason);
+        }
+    }
+
+    private void reportFullyLoaded() {
+        final Callback callback;
+        synchronized (mLock) {
+            callback = mCallback;
+        }
+        if (callback != null) {
+            callback.onPackageFullyLoaded();
+        }
+    }
+
+    private class StatusConsumer implements BiConsumer<Integer, Integer> {
+        @Override
+        public void accept(Integer streamStatus, Integer storageStatus) {
+            if (streamStatus == null && storageStatus == null) {
+                return;
+            }
+            final boolean oldState, newState;
+            synchronized (mLock) {
+                if (!mLoadingState.isLoading()) {
+                    // Do nothing if the package is already fully loaded
+                    return;
+                }
+                oldState = mStartableState.isStartable();
+                if (streamStatus != null) {
+                    mStreamStatus = (Integer) streamStatus;
+                }
+                if (storageStatus != null) {
+                    mStorageHealthStatus = (Integer) storageStatus;
+                }
+                updateStartableStateLocked();
+                newState = mStartableState.isStartable();
+            }
+            if (oldState != newState) {
+                mHandler.post(PooledLambda.obtainRunnable(IncrementalStates::reportStartableState,
+                        IncrementalStates.this).recycleOnUse());
+            }
+        }
+    };
+
+    /**
+     * By calling this method, the caller indicates that there issues with the Incremental
+     * Storage,
+     * on which the package is installed. The state will change according to the status
+     * code defined in {@code IStorageHealthListener}.
+     */
+    public void onStorageHealthStatusChanged(int storageHealthStatus) {
+        if (DEBUG) {
+            Slog.i(TAG, "received storage health status changed event : storageHealthStatus="
+                    + storageHealthStatus);
+        }
+        mStatusConsumer.accept(null, storageHealthStatus);
+    }
+
+    /**
+     * By calling this method, the caller indicates that the stream status of the package has
+     * been
+     * changed. This could indicate a streaming error. The state will change according to the
+     * status
+     * code defined in {@code IDataLoaderStatusListener}.
+     */
+    public void onStreamStatusChanged(int streamState) {
+        if (DEBUG) {
+            Slog.i(TAG, "received stream status changed event : streamState=" + streamState);
+        }
+        mStatusConsumer.accept(streamState, null);
+    }
+
+    /**
+     * Use the specified callback to report state changing events.
+     *
+     * @param callback Object to report new state.
+     */
+    public void setCallback(Callback callback) {
+        if (DEBUG) {
+            Slog.i(TAG, "registered callback");
+        }
+        synchronized (mLock) {
+            mCallback = callback;
+        }
+    }
+
+    /**
+     * Update the package loading progress to specified value. This might change startable state.
+     *
+     * @param progress Value between [0, 1].
+     */
+    public void setProgress(float progress) {
+        final boolean newLoadingState;
+        final boolean oldStartableState, newStartableState;
+        synchronized (mLock) {
+            oldStartableState = mStartableState.isStartable();
+            updateProgressLocked(progress);
+            newLoadingState = mLoadingState.isLoading();
+            newStartableState = mStartableState.isStartable();
+        }
+        if (!newLoadingState) {
+            mHandler.post(PooledLambda.obtainRunnable(
+                    IncrementalStates::reportFullyLoaded,
+                    IncrementalStates.this).recycleOnUse());
+        }
+        if (newStartableState != oldStartableState) {
+            mHandler.post(PooledLambda.obtainRunnable(
+                    IncrementalStates::reportStartableState,
+                    IncrementalStates.this).recycleOnUse());
+        }
+    }
+
+    /**
+     * @return the current startable state.
+     */
+    public boolean isStartable() {
+        synchronized (mLock) {
+            return mStartableState.isStartable();
+        }
+    }
+
+    /**
+     * @return Whether the package is still being loaded or has been fully loaded.
+     */
+    public boolean isLoading() {
+        synchronized (mLock) {
+            return mLoadingState.isLoading();
+        }
+    }
+
+    /**
+     * @return all current states in a Parcelable.
+     */
+    public IncrementalStatesInfo getIncrementalStatesInfo() {
+        synchronized (mLock) {
+            return new IncrementalStatesInfo(mStartableState.isStartable(),
+                    mLoadingState.isLoading(),
+                    mLoadingState.getProgress());
+        }
+    }
+
+    /**
+     * Determine the next state based on the current state, current stream status and storage
+     * health
+     * status. If the next state is different from the current state, proceed with state
+     * change.
+     */
+    private void updateStartableStateLocked() {
+        final boolean currentState = mStartableState.isStartable();
+        boolean nextState = currentState;
+        if (!currentState) {
+            if (mStorageHealthStatus == IStorageHealthListener.HEALTH_STATUS_OK
+                    && mStreamStatus == IDataLoaderStatusListener.STREAM_HEALTHY) {
+                // change from unstartable -> startable when both stream and storage are healthy
+                nextState = true;
+            }
+        } else {
+            if (mStorageHealthStatus == IStorageHealthListener.HEALTH_STATUS_UNHEALTHY) {
+                // unrecoverable if storage is unhealthy
+                nextState = false;
+            } else {
+                switch (mStreamStatus) {
+                    case IDataLoaderStatusListener.STREAM_INTEGRITY_ERROR:
+                        // unrecoverable, fall through
+                    case IDataLoaderStatusListener.STREAM_SOURCE_ERROR: {
+                        // unrecoverable
+                        nextState = false;
+                        break;
+                    }
+                    case IDataLoaderStatusListener.STREAM_STORAGE_ERROR: {
+                        if (mStorageHealthStatus != IStorageHealthListener.HEALTH_STATUS_OK) {
+                            // unrecoverable if there is a pending read AND storage is limited
+                            nextState = false;
+                        }
+                        break;
+                    }
+                    default:
+                        // anything else, remain startable
+                        break;
+                }
+            }
+        }
+        if (nextState == currentState) {
+            return;
+        }
+        mStartableState.adoptNewStartableStateLocked(nextState);
+    }
+
+    private void updateProgressLocked(float progress) {
+        if (DEBUG) {
+            Slog.i(TAG, "received progress update: " + progress);
+        }
+        mLoadingState.setProgress(progress);
+        if (1 - progress < 0.001) {
+            if (DEBUG) {
+                Slog.i(TAG, "package is fully loaded");
+            }
+            mLoadingState.setProgress(1);
+            if (mLoadingState.isLoading()) {
+                mLoadingState.adoptNewLoadingStateLocked(false);
+            }
+            // Also updates startable state if necessary
+            if (!mStartableState.isStartable()) {
+                mStartableState.adoptNewStartableStateLocked(true);
+            }
+        }
+    }
+
+    private class StartableState {
+        private boolean mIsStartable;
+        private int mUnstartableReason = PackageManager.UNSTARTABLE_REASON_UNKNOWN;
+
+        StartableState(boolean isStartable) {
+            mIsStartable = isStartable;
+        }
+
+        public boolean isStartable() {
+            return mIsStartable;
+        }
+
+        public int getUnstartableReason() {
+            return mUnstartableReason;
+        }
+
+        public void adoptNewStartableStateLocked(boolean nextState) {
+            if (DEBUG) {
+                Slog.i(TAG, "startable state changed from " + mIsStartable + " to " + nextState);
+            }
+            mIsStartable = nextState;
+            mUnstartableReason = getUnstartableReasonLocked();
+        }
+
+        private int getUnstartableReasonLocked() {
+            if (mIsStartable) {
+                return PackageManager.UNSTARTABLE_REASON_UNKNOWN;
+            }
+            // Translate stream status to reason for unstartable state
+            switch (mStreamStatus) {
+                case IDataLoaderStatusListener.STREAM_TRANSPORT_ERROR:
+                    // fall through
+                case IDataLoaderStatusListener.STREAM_INTEGRITY_ERROR:
+                    // fall through
+                case IDataLoaderStatusListener.STREAM_SOURCE_ERROR: {
+                    return PackageManager.UNSTARTABLE_REASON_DATALOADER_TRANSPORT;
+                }
+                case IDataLoaderStatusListener.STREAM_STORAGE_ERROR: {
+                    return PackageManager.UNSTARTABLE_REASON_DATALOADER_STORAGE;
+                }
+                default:
+                    return PackageManager.UNSTARTABLE_REASON_UNKNOWN;
+            }
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == this) {
+                return true;
+            }
+            if (!(o instanceof StartableState)) {
+                return false;
+            }
+            StartableState l = (StartableState) o;
+            return l.mIsStartable == mIsStartable;
+        }
+
+        @Override
+        public int hashCode() {
+            return Boolean.hashCode(mIsStartable);
+        }
+    }
+
+    private class LoadingState {
+        private boolean mIsLoading;
+        private float mProgress;
+
+        LoadingState(boolean isLoading) {
+            mIsLoading = isLoading;
+            mProgress = isLoading ? 0 : 1;
+        }
+
+        public boolean isLoading() {
+            return mIsLoading;
+        }
+
+        public float getProgress() {
+            return mProgress;
+        }
+
+        public void setProgress(float progress) {
+            mProgress = progress;
+        }
+
+        public void adoptNewLoadingStateLocked(boolean nextState) {
+            if (DEBUG) {
+                Slog.i(TAG, "Loading state changed from " + mIsLoading + " to " + nextState);
+            }
+            mIsLoading = nextState;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == this) {
+                return true;
+            }
+            if (!(o instanceof LoadingState)) {
+                return false;
+            }
+            LoadingState l = (LoadingState) o;
+            return l.mIsLoading == mIsLoading && l.mProgress == mProgress;
+        }
+
+        @Override
+        public int hashCode() {
+            int hashCode = Boolean.hashCode(mIsLoading);
+            hashCode = 31 * hashCode + Float.hashCode(mProgress);
+            return hashCode;
+        }
+    }
+
+
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (!(o instanceof IncrementalStates)) {
+            return false;
+        }
+        IncrementalStates l = (IncrementalStates) o;
+        return l.mStorageHealthStatus == mStorageHealthStatus
+                && l.mStreamStatus == mStreamStatus
+                && l.mStartableState.equals(mStartableState)
+                && l.mLoadingState.equals(mLoadingState);
+    }
+
+    @Override
+    public int hashCode() {
+        int hashCode = mStartableState.hashCode();
+        hashCode = 31 * hashCode + mLoadingState.hashCode();
+        hashCode = 31 * hashCode + mStorageHealthStatus;
+        hashCode = 31 * hashCode + mStreamStatus;
+        return hashCode;
+    }
+}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 949dcb25..bee9ef9 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -141,6 +141,7 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
+import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.LocalServices;
 import com.android.server.pm.Installer.InstallerException;
 import com.android.server.pm.dex.DexManager;
@@ -1684,20 +1685,26 @@
         }
     }
 
-    private void onStorageUnhealthy() {
+    private void onStorageHealthStatusChanged(int status) {
         final String packageName = getPackageName();
         if (TextUtils.isEmpty(packageName)) {
             // The package has not been installed.
             return;
         }
-        final PackageManagerService packageManagerService = mPm;
-        mHandler.post(() -> {
-            if (packageManagerService.deletePackageX(packageName,
-                    PackageManager.VERSION_CODE_HIGHEST, UserHandle.USER_SYSTEM,
-                    PackageManager.DELETE_ALL_USERS) != PackageManager.DELETE_SUCCEEDED) {
-                Slog.e(TAG, "Failed to uninstall package with failed dataloader: " + packageName);
-            }
-        });
+        mHandler.post(PooledLambda.obtainRunnable(
+                PackageManagerService::onStorageHealthStatusChanged,
+                mPm, packageName, status, userId).recycleOnUse());
+    }
+
+    private void onStreamHealthStatusChanged(int status) {
+        final String packageName = getPackageName();
+        if (TextUtils.isEmpty(packageName)) {
+            // The package has not been installed.
+            return;
+        }
+        mHandler.post(PooledLambda.obtainRunnable(
+                PackageManagerService::onStreamStatusChanged,
+                mPm, packageName, status, userId).recycleOnUse());
     }
 
     /**
@@ -3245,7 +3252,9 @@
                 if (isDestroyedOrDataLoaderFinished) {
                     switch (status) {
                         case IDataLoaderStatusListener.DATA_LOADER_UNRECOVERABLE:
-                            onStorageUnhealthy();
+                            // treat as unhealthy storage
+                            onStorageHealthStatusChanged(
+                                    IStorageHealthListener.HEALTH_STATUS_UNHEALTHY);
                             return;
                     }
                     return;
@@ -3342,6 +3351,16 @@
                     sendPendingStreaming(mContext, statusReceiver, sessionId, e.getMessage());
                 }
             }
+            @Override
+            public void reportStreamHealth(int dataLoaderId, int streamStatus) {
+                synchronized (mLock) {
+                    if (!mDestroyed && !mDataLoaderFinished) {
+                        // ignore streaming status if package isn't installed
+                        return;
+                    }
+                }
+                onStreamHealthStatusChanged(streamStatus);
+            }
         };
 
         if (!manualStartAndDestroy) {
@@ -3361,11 +3380,7 @@
                     }
                     if (isDestroyedOrDataLoaderFinished) {
                         // App's installed.
-                        switch (status) {
-                            case IStorageHealthListener.HEALTH_STATUS_UNHEALTHY:
-                                onStorageUnhealthy();
-                                return;
-                        }
+                        onStorageHealthStatusChanged(status);
                         return;
                     }
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 1ae1681..93b567f 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -16390,12 +16390,24 @@
                 } else if (!previousUserIds.contains(userId)) {
                     ps.setInstallReason(installReason, userId);
                 }
+
+                // TODO(b/169721400): generalize Incremental States and create a Callback object
+                // that can be used for all the packages.
+                final IncrementalStatesCallback incrementalStatesCallback =
+                        new IncrementalStatesCallback(ps, userId);
+                final String codePath = ps.getPathString();
+                if (IncrementalManager.isIncrementalPath(codePath) && mIncrementalManager != null) {
+                    mIncrementalManager.registerCallback(codePath, incrementalStatesCallback);
+                    ps.setIncrementalStatesCallback(incrementalStatesCallback);
+                }
+
                 // Ensure that the uninstall reason is UNKNOWN for users with the package installed.
                 for (int currentUserId : allUsersList) {
                     if (ps.getInstalled(currentUserId)) {
                         ps.setUninstallReason(UNINSTALL_REASON_UNKNOWN, currentUserId);
                     }
                 }
+
                 mSettings.writeKernelMappingLPr(ps);
             }
             res.name = pkgName;
@@ -17192,6 +17204,7 @@
                 mDexManager.notifyPackageUpdated(pkg.getPackageName(),
                         pkg.getBaseApkPath(), pkg.getSplitCodePaths());
             }
+            reconciledPkg.pkgSetting.setStatesOnCommit();
 
             // Prepare the application profiles for the new code paths.
             // This needs to be done before invoking dexopt so that any install-time profile
@@ -17288,6 +17301,155 @@
         NativeLibraryHelper.waitForNativeBinariesExtraction(incrementalStorages);
     }
 
+    private class IncrementalStatesCallback extends IPackageLoadingProgressCallback.Stub
+            implements IncrementalStates.Callback {
+        @GuardedBy("mPackageSetting")
+        private final PackageSetting mPackageSetting;
+        private final String mPackageName;
+        private final String mPathString;
+        private final int mUserId;
+        private final int[] mInstalledUserIds;
+
+        IncrementalStatesCallback(PackageSetting packageSetting, int userId) {
+            mPackageSetting = packageSetting;
+            mPackageName = packageSetting.name;
+            mUserId = userId;
+            mPathString = packageSetting.getPathString();
+            final int[] allUserIds = resolveUserIds(userId);
+            final ArrayList<Integer> installedUserIds = new ArrayList<>();
+            for (int i = 0; i < allUserIds.length; i++) {
+                if (packageSetting.getInstalled(allUserIds[i])) {
+                    installedUserIds.add(allUserIds[i]);
+                }
+            }
+            final int numInstalledUserId = installedUserIds.size();
+            mInstalledUserIds = new int[numInstalledUserId];
+            for (int i = 0; i < numInstalledUserId; i++) {
+                mInstalledUserIds[i] = installedUserIds.get(i);
+            }
+        }
+
+        @Override
+        public void onPackageLoadingProgressChanged(float progress) {
+            synchronized (mPackageSetting) {
+                mPackageSetting.setLoadingProgress(progress);
+            }
+        }
+
+        @Override
+        public void onPackageFullyLoaded() {
+            mIncrementalManager.unregisterCallback(mPathString, this);
+            final SparseArray<int[]> newBroadcastAllowList;
+            synchronized (mLock) {
+                newBroadcastAllowList = mAppsFilter.getVisibilityAllowList(
+                        getPackageSettingInternal(mPackageName, Process.SYSTEM_UID),
+                        mInstalledUserIds, mSettings.mPackages);
+            }
+            Bundle extras = new Bundle(1);
+            extras.putInt(Intent.EXTRA_UID, mUserId);
+            extras.putString(Intent.EXTRA_PACKAGE_NAME, mPackageName);
+            sendPackageBroadcast(Intent.ACTION_PACKAGE_FULLY_LOADED, mPackageName,
+                    extras, 0 /*flags*/,
+                    null /*targetPackage*/, null /*finishedReceiver*/,
+                    mInstalledUserIds, null /* instantUserIds */, newBroadcastAllowList);
+        }
+
+        @Override
+        public void onPackageUnstartable(int reason) {
+            final SparseArray<int[]> newBroadcastAllowList;
+            synchronized (mLock) {
+                newBroadcastAllowList = mAppsFilter.getVisibilityAllowList(
+                        getPackageSettingInternal(mPackageName, Process.SYSTEM_UID),
+                        mInstalledUserIds, mSettings.mPackages);
+            }
+            Bundle extras = new Bundle(1);
+            extras.putInt(Intent.EXTRA_UID, mUserId);
+            extras.putString(Intent.EXTRA_PACKAGE_NAME, mPackageName);
+            extras.putInt(Intent.EXTRA_REASON, reason);
+            // send broadcast to users with this app installed
+            sendPackageBroadcast(Intent.ACTION_PACKAGE_UNSTARTABLE, mPackageName,
+                    extras, 0 /*flags*/,
+                    null /*targetPackage*/, null /*finishedReceiver*/,
+                    mInstalledUserIds, null /* instantUserIds */, newBroadcastAllowList);
+        }
+
+        @Override
+        public void onPackageStartable() {
+            final SparseArray<int[]> newBroadcastAllowList;
+            synchronized (mLock) {
+                newBroadcastAllowList = mAppsFilter.getVisibilityAllowList(
+                        getPackageSettingInternal(mPackageName, Process.SYSTEM_UID),
+                        mInstalledUserIds, mSettings.mPackages);
+            }
+            Bundle extras = new Bundle(1);
+            extras.putInt(Intent.EXTRA_UID, mUserId);
+            extras.putString(Intent.EXTRA_PACKAGE_NAME, mPackageName);
+            // send broadcast to users with this app installed
+            sendPackageBroadcast(Intent.ACTION_PACKAGE_STARTABLE, mPackageName,
+                    extras, 0 /*flags*/,
+                    null /*targetPackage*/, null /*finishedReceiver*/,
+                    mInstalledUserIds, null /* instantUserIds */, newBroadcastAllowList);
+        }
+    }
+
+    /**
+     * This is an internal method that is used to indicate changes on the health status of the
+     * Incremental Storage used by an installed package with an associated user id. This might
+     * result in a change in the loading state of the package.
+     */
+    public void onStorageHealthStatusChanged(String packageName, int status, int userId) {
+        final int callingUid = Binder.getCallingUid();
+        mPermissionManager.enforceCrossUserPermission(
+                callingUid, userId, true, false,
+                "onStorageHealthStatusChanged");
+        final PackageSetting ps = getPackageSettingForUser(packageName, callingUid, userId);
+        if (ps == null) {
+            return;
+        }
+        ps.setStorageHealthStatus(status);
+    }
+
+    /**
+     * This is an internal method that is used to indicate changes on the stream status of the
+     * data loader used by an installed package with an associated user id. This might
+     * result in a change in the loading state of the package.
+     */
+    public void onStreamStatusChanged(String packageName, int status, int userId) {
+        final int callingUid = Binder.getCallingUid();
+        mPermissionManager.enforceCrossUserPermission(
+                callingUid, userId, true, false,
+                "onStreamStatusChanged");
+        final PackageSetting ps = getPackageSettingForUser(packageName, callingUid, userId);
+        if (ps == null) {
+            return;
+        }
+        ps.setStreamStatus(status);
+    }
+
+    @Nullable PackageSetting getPackageSettingForUser(String packageName, int callingUid,
+            int userId) {
+        final PackageSetting ps;
+        synchronized (mLock) {
+            ps = mSettings.mPackages.get(packageName);
+            if (ps == null) {
+                Slog.w(TAG, "Failed to get package setting. Package " + packageName
+                        + " is not installed");
+                return null;
+            }
+            if (!ps.getInstalled(userId)) {
+                Slog.w(TAG, "Failed to get package setting. Package " + packageName
+                        + " is not installed for user " + userId);
+                return null;
+            }
+            if (shouldFilterApplicationLocked(ps, callingUid, userId)) {
+                Slog.w(TAG, "Failed to get package setting. Package " + packageName
+                        + " is not visible to the calling app");
+                return null;
+            }
+        }
+        return ps;
+    }
+
     private void notifyPackageChangeObserversOnUpdate(ReconciledPackage reconciledPkg) {
       final PackageSetting pkgSetting = reconciledPkg.pkgSetting;
       final PackageInstalledInfo pkgInstalledInfo = reconciledPkg.installResult;
@@ -25407,24 +25569,15 @@
         @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
+            final PackageSetting ps = getPackageSettingForUser(packageName, Binder.getCallingUid(),
+                    userId);
+            if (ps == null) {
+                return false;
+            }
+            if (!ps.isPackageLoading()) {
+                Slog.w(TAG,
+                        "Failed registering loading progress callback. Package is fully loaded.");
+                return false;
             }
             if (mIncrementalManager == null) {
                 Slog.w(TAG,
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 855a5ff5..2ff18f8 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -333,6 +333,8 @@
                     installSource.originatingPackageName);
             proto.end(sourceToken);
         }
+        proto.write(PackageProto.StatesProto.IS_STARTABLE, incrementalStates.isStartable());
+        proto.write(PackageProto.StatesProto.IS_LOADING, incrementalStates.isLoading());
         writeUsersInfoToProto(proto, PackageProto.USERS);
         proto.end(packageToken);
     }
diff --git a/services/core/java/com/android/server/pm/PackageSettingBase.java b/services/core/java/com/android/server/pm/PackageSettingBase.java
index a7bbf8d..d52ad46 100644
--- a/services/core/java/com/android/server/pm/PackageSettingBase.java
+++ b/services/core/java/com/android/server/pm/PackageSettingBase.java
@@ -25,6 +25,7 @@
 import android.annotation.UserIdInt;
 import android.content.ComponentName;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.IncrementalStatesInfo;
 import android.content.pm.IntentFilterVerificationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.UninstallReason;
@@ -33,6 +34,7 @@
 import android.content.pm.Signature;
 import android.content.pm.SuspendDialogInfo;
 import android.os.PersistableBundle;
+import android.os.incremental.IncrementalManager;
 import android.service.pm.PackageProto;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -133,6 +135,9 @@
 
     boolean forceQueryableOverride;
 
+    @NonNull
+    public IncrementalStates incrementalStates;
+
     PackageSettingBase(String name, String realName, @NonNull File path,
             String legacyNativeLibraryPathString, String primaryCpuAbiString,
             String secondaryCpuAbiString, String cpuAbiOverrideString,
@@ -151,6 +156,7 @@
         this.versionCode = pVersionCode;
         this.signatures = new PackageSignatures();
         this.installSource = InstallSource.EMPTY;
+        this.incrementalStates = new IncrementalStates();
     }
 
     /**
@@ -257,6 +263,7 @@
                        orig.usesStaticLibrariesVersions.length) : null;
         updateAvailable = orig.updateAvailable;
         forceQueryableOverride = orig.forceQueryableOverride;
+        incrementalStates = orig.incrementalStates;
     }
 
     @VisibleForTesting
@@ -733,6 +740,66 @@
         modifyUserState(userId).resetOverrideComponentLabelIcon();
     }
 
+    /**
+     * @return True if package is startable, false otherwise.
+     */
+    public boolean isPackageStartable() {
+        return incrementalStates.isStartable();
+    }
+
+    /**
+     * @return True if package is still being loaded, false if the package is fully loaded.
+     */
+    public boolean isPackageLoading() {
+        return incrementalStates.isLoading();
+    }
+
+    /**
+     * @return all current states in a Parcelable.
+     */
+    public IncrementalStatesInfo getIncrementalStates() {
+        return incrementalStates.getIncrementalStatesInfo();
+    }
+
+    /**
+     * Called to indicate that the package installation has been committed. This will create a
+     * new startable state and a new loading state with default values. By default, the package is
+     * startable after commit. For a package installed on Incremental, the loading state is true.
+     * For non-Incremental packages, the loading state is false.
+     */
+    public void setStatesOnCommit() {
+        incrementalStates.onCommit(IncrementalManager.isIncrementalPath(getPathString()));
+    }
+
+    /**
+     * Called to set the callback to listen for startable state changes.
+     */
+    public void setIncrementalStatesCallback(IncrementalStates.Callback callback) {
+        incrementalStates.setCallback(callback);
+    }
+
+    /**
+     * Called to report progress changes. This might trigger loading state change.
+     * @see IncrementalStates#setProgress(float)
+     */
+    public void setLoadingProgress(float progress) {
+        incrementalStates.setProgress(progress);
+    }
+
+    /**
+     * @see IncrementalStates#onStorageHealthStatusChanged(int)
+     */
+    public void setStorageHealthStatus(int status) {
+        incrementalStates.onStorageHealthStatusChanged(status);
+    }
+
+    /**
+     * @see IncrementalStates#onStreamStatusChanged(int)
+     */
+    public void setStreamStatus(int status) {
+        incrementalStates.onStreamStatusChanged(status);
+    }
+
     protected PackageSettingBase updateFrom(PackageSettingBase other) {
         super.copyFrom(other);
         setPath(other.getPath());
@@ -756,6 +823,7 @@
         this.updateAvailable = other.updateAvailable;
         this.verificationInfo = other.verificationInfo;
         this.forceQueryableOverride = other.forceQueryableOverride;
+        this.incrementalStates = other.incrementalStates;
 
         if (mOldCodePaths != null) {
             if (other.mOldCodePaths != null) {
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index a922d76..c16bd5c 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -2809,6 +2809,12 @@
         if (pkg.forceQueryableOverride) {
             serializer.attribute(null, "forceQueryable", "true");
         }
+        if (pkg.isPackageStartable()) {
+            serializer.attribute(null, "isStartable", "true");
+        }
+        if (pkg.isPackageLoading()) {
+            serializer.attribute(null, "isLoading", "true");
+        }
 
         writeUsesStaticLibLPw(serializer, pkg.usesStaticLibraries, pkg.usesStaticLibrariesVersions);
 
@@ -3594,6 +3600,8 @@
         String version = null;
         long versionCode = 0;
         String installedForceQueryable = null;
+        String isStartable = null;
+        String isLoading = null;
         try {
             name = parser.getAttributeValue(null, ATTR_NAME);
             realName = parser.getAttributeValue(null, "realName");
@@ -3610,6 +3618,8 @@
             cpuAbiOverrideString = parser.getAttributeValue(null, "cpuAbiOverride");
             updateAvailable = parser.getAttributeValue(null, "updateAvailable");
             installedForceQueryable = parser.getAttributeValue(null, "forceQueryable");
+            isStartable = parser.getAttributeValue(null, "isStartable");
+            isLoading = parser.getAttributeValue(null, "isLoading");
 
             if (primaryCpuAbiString == null && legacyCpuAbiString != null) {
                 primaryCpuAbiString = legacyCpuAbiString;
@@ -3793,6 +3803,8 @@
             packageSetting.secondaryCpuAbiString = secondaryCpuAbiString;
             packageSetting.updateAvailable = "true".equals(updateAvailable);
             packageSetting.forceQueryableOverride = "true".equals(installedForceQueryable);
+            packageSetting.incrementalStates = new IncrementalStates("true".equals(isStartable),
+                    "true".equals(isLoading));
             // Handle legacy string here for single-user mode
             final String enabledStr = parser.getAttributeValue(null, ATTR_ENABLED);
             if (enabledStr != null) {
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index bbcb312..5f145f3 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -2135,6 +2135,11 @@
     return binder::Status::ok();
 }
 
+binder::Status IncrementalService::DataLoaderStub::reportStreamHealth(MountId mountId,
+                                                                      int newStatus) {
+    return binder::Status::ok();
+}
+
 bool IncrementalService::DataLoaderStub::isHealthParamsValid() const {
     return mHealthCheckParams.blockedTimeoutMs > 0 &&
             mHealthCheckParams.blockedTimeoutMs < mHealthCheckParams.unhealthyTimeoutMs;
diff --git a/services/incremental/IncrementalService.h b/services/incremental/IncrementalService.h
index d820417..504c02a 100644
--- a/services/incremental/IncrementalService.h
+++ b/services/incremental/IncrementalService.h
@@ -200,6 +200,7 @@
 
     private:
         binder::Status onStatusChanged(MountId mount, int newStatus) final;
+        binder::Status reportStreamHealth(MountId mount, int newStatus) final;
 
         sp<content::pm::IDataLoader> getDataLoader();