[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();