diff options
author | 2020-09-10 13:12:39 -0700 | |
---|---|---|
committer | 2020-10-01 16:36:25 -0700 | |
commit | 3309398cbfe33452a3568a7ab7d80e127b63a10c (patch) | |
tree | 6d787b0ed7fd64061eb9c53434931d811fbb4bee | |
parent | f2c9466eaa819654f6535f1b260493769ca116a3 (diff) |
[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
16 files changed, 979 insertions, 34 deletions
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 35c7b968c892..9a9f165030b0 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -2732,6 +2732,54 @@ public class Intent implements Parcelable, Cloneable { 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 efb00a016da5..745c39b460fa 100644 --- a/core/java/android/content/pm/IDataLoaderStatusListener.aidl +++ b/core/java/android/content/pm/IDataLoaderStatusListener.aidl @@ -50,7 +50,30 @@ oneway interface IDataLoaderStatusListener { * 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 000000000000..b5aad1269c60 --- /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 000000000000..6e91c19affc2 --- /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 79e23b37be05..0e47b06a6faf 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -3738,6 +3738,39 @@ public abstract class PackageManager { */ 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 327d1b8beeb1..3ed21b087972 100644 --- a/core/java/android/content/pm/PackageUserState.java +++ b/core/java/android/content/pm/PackageUserState.java @@ -44,7 +44,6 @@ import android.util.Slog; 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 004b096496db..d289e00d3467 100644 --- a/core/proto/android/service/package.proto +++ b/core/proto/android/service/package.proto @@ -124,6 +124,11 @@ message PackageProto { 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 @@ message PackageProto { 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 723cceba90bc..846ed871631f 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 000000000000..dda5fafb8cb1 --- /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 949dcb254d21..bee9ef96c455 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.ArrayUtils; 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } - 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } 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 1ae1681f6771..93b567fb7043 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 @@ public class PackageManagerService extends IPackageManager.Stub } 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 @@ public class PackageManagerService extends IPackageManager.Stub 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 @@ public class PackageManagerService extends IPackageManager.Stub 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 @@ public class PackageManagerService extends IPackageManager.Stub @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 855a5ff524fd..2ff18f8bdf79 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 @@ public class PackageSetting extends PackageSettingBase { 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 a7bbf8d66aac..d52ad46d4b7e 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.Nullable; 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.PackageUserState; 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 @@ public abstract class PackageSettingBase extends SettingBase { 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 @@ public abstract class PackageSettingBase extends SettingBase { this.versionCode = pVersionCode; this.signatures = new PackageSignatures(); this.installSource = InstallSource.EMPTY; + this.incrementalStates = new IncrementalStates(); } /** @@ -257,6 +263,7 @@ public abstract class PackageSettingBase extends SettingBase { orig.usesStaticLibrariesVersions.length) : null; updateAvailable = orig.updateAvailable; forceQueryableOverride = orig.forceQueryableOverride; + incrementalStates = orig.incrementalStates; } @VisibleForTesting @@ -733,6 +740,66 @@ public abstract class PackageSettingBase extends SettingBase { 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 @@ public abstract class PackageSettingBase extends SettingBase { 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 a922d76cf9eb..c16bd5cdf1fa 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 @@ public final class Settings { 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 @@ public final class Settings { 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 @@ public final class Settings { 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 @@ public final class Settings { 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 bbcb3122c9bb..5f145f33f628 100644 --- a/services/incremental/IncrementalService.cpp +++ b/services/incremental/IncrementalService.cpp @@ -2135,6 +2135,11 @@ binder::Status IncrementalService::DataLoaderStub::onStatusChanged(MountId mount 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 d820417e73ed..504c02a57b86 100644 --- a/services/incremental/IncrementalService.h +++ b/services/incremental/IncrementalService.h @@ -200,6 +200,7 @@ private: private: binder::Status onStatusChanged(MountId mount, int newStatus) final; + binder::Status reportStreamHealth(MountId mount, int newStatus) final; sp<content::pm::IDataLoader> getDataLoader(); |