diff options
| -rw-r--r-- | core/api/current.txt | 30 | ||||
| -rw-r--r-- | core/java/android/content/pm/IPackageInstaller.aidl | 3 | ||||
| -rw-r--r-- | core/java/android/content/pm/PackageInstaller.aidl | 1 | ||||
| -rw-r--r-- | core/java/android/content/pm/PackageInstaller.java | 384 | ||||
| -rw-r--r-- | core/res/AndroidManifest.xml | 4 | ||||
| -rw-r--r-- | services/core/java/com/android/server/pm/AppStateHelper.java | 172 | ||||
| -rw-r--r-- | services/core/java/com/android/server/pm/GentleUpdateHelper.java | 171 | ||||
| -rw-r--r-- | services/core/java/com/android/server/pm/PackageInstallerService.java | 39 | ||||
| -rw-r--r-- | services/core/java/com/android/server/pm/PackageSessionProvider.java | 5 |
9 files changed, 809 insertions, 0 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index ed63d6fcdcb8..ac306e91f6d7 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -11664,6 +11664,7 @@ package android.content.pm { public class PackageInstaller { method public void abandonSession(int); + method public void checkInstallConstraints(@NonNull java.util.List<java.lang.String>, @NonNull android.content.pm.PackageInstaller.InstallConstraints, @NonNull java.util.function.Consumer<android.content.pm.PackageInstaller.InstallConstraintsResult>); method public int createSession(@NonNull android.content.pm.PackageInstaller.SessionParams) throws java.io.IOException; method @Deprecated @Nullable public android.content.pm.PackageInstaller.SessionInfo getActiveStagedSession(); method @NonNull public java.util.List<android.content.pm.PackageInstaller.SessionInfo> getActiveStagedSessions(); @@ -11708,6 +11709,35 @@ package android.content.pm { field public static final int STATUS_SUCCESS = 0; // 0x0 } + public static final class PackageInstaller.InstallConstraints implements android.os.Parcelable { + method public int describeContents(); + method public boolean isRequireAppNotForeground(); + method public boolean isRequireAppNotInteracting(); + method public boolean isRequireAppNotTopVisible(); + method public boolean isRequireDeviceIdle(); + method public boolean isRequireNotInCall(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.PackageInstaller.InstallConstraints> CREATOR; + field @NonNull public static final android.content.pm.PackageInstaller.InstallConstraints GENTLE_UPDATE; + } + + public static final class PackageInstaller.InstallConstraints.Builder { + ctor public PackageInstaller.InstallConstraints.Builder(); + method @NonNull public android.content.pm.PackageInstaller.InstallConstraints build(); + method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotForeground(); + method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotInteracting(); + method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotTopVisible(); + method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder requireDeviceIdle(); + method @NonNull public android.content.pm.PackageInstaller.InstallConstraints.Builder requireNotInCall(); + } + + public static final class PackageInstaller.InstallConstraintsResult implements android.os.Parcelable { + method public int describeContents(); + method public boolean isAllConstraintsSatisfied(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.PackageInstaller.InstallConstraintsResult> CREATOR; + } + public static final class PackageInstaller.PreapprovalDetails implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.graphics.Bitmap getIcon(); diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl index 12911d6e1232..1e928bd6c9be 100644 --- a/core/java/android/content/pm/IPackageInstaller.aidl +++ b/core/java/android/content/pm/IPackageInstaller.aidl @@ -23,6 +23,7 @@ import android.content.pm.PackageInstaller; import android.content.pm.ParceledListSlice; import android.content.pm.VersionedPackage; import android.content.IntentSender; +import android.os.RemoteCallback; import android.graphics.Bitmap; @@ -66,4 +67,6 @@ interface IPackageInstaller { void setAllowUnlimitedSilentUpdates(String installerPackageName); void setSilentUpdatesThrottleTime(long throttleTimeInSeconds); + void checkInstallConstraints(String installerPackageName, in List<String> packageNames, + in PackageInstaller.InstallConstraints constraints, in RemoteCallback callback); } diff --git a/core/java/android/content/pm/PackageInstaller.aidl b/core/java/android/content/pm/PackageInstaller.aidl index 833919e16855..ab9d4f3194ca 100644 --- a/core/java/android/content/pm/PackageInstaller.aidl +++ b/core/java/android/content/pm/PackageInstaller.aidl @@ -16,6 +16,7 @@ package android.content.pm; +parcelable PackageInstaller.InstallConstraints; parcelable PackageInstaller.SessionParams; parcelable PackageInstaller.SessionInfo; parcelable PackageInstaller.PreapprovalDetails; diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index d7686e22756e..104d2791c622 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -36,6 +36,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.ActivityManager; @@ -57,6 +58,7 @@ import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.ParcelableException; +import android.os.RemoteCallback; import android.os.RemoteException; import android.os.SystemProperties; import android.os.UserHandle; @@ -89,6 +91,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Offers the ability to install, upgrade, and remove applications on the @@ -854,6 +857,29 @@ public class PackageInstaller { } /** + * Check if install constraints are satisfied for the given packages. + * + * Note this query result is just a hint and subject to race because system states could + * change anytime in-between this query and committing the session. + * + * The result is returned by a callback because some constraints might take a long time + * to evaluate. + */ + public void checkInstallConstraints(@NonNull List<String> packageNames, + @NonNull InstallConstraints constraints, + @NonNull Consumer<InstallConstraintsResult> callback) { + try { + var remoteCallback = new RemoteCallback(b -> { + callback.accept(b.getParcelable("result", InstallConstraintsResult.class)); + }); + mInstaller.checkInstallConstraints( + mInstallerPackageName, packageNames, constraints, remoteCallback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Events for observing session lifecycle. * <p> * A typical session lifecycle looks like this: @@ -3608,4 +3634,362 @@ public class PackageInstaller { // End of generated code } + + /** + * The callback result of {@link #checkInstallConstraints(List, InstallConstraints, Consumer)}. + */ + @DataClass(genParcelable = true, genHiddenConstructor = true) + public static final class InstallConstraintsResult implements Parcelable { + /** + * True if all constraints are satisfied. + */ + private boolean mAllConstraintsSatisfied; + + + + // Code below generated by codegen v1.0.23. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/content/pm/PackageInstaller.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + /** + * Creates a new InstallConstraintsResult. + * + * @param allConstraintsSatisfied + * True if all constraints are satisfied. + * @hide + */ + @DataClass.Generated.Member + public InstallConstraintsResult( + boolean allConstraintsSatisfied) { + this.mAllConstraintsSatisfied = allConstraintsSatisfied; + + // onConstructed(); // You can define this method to get a callback + } + + /** + * True if all constraints are satisfied. + */ + @DataClass.Generated.Member + public boolean isAllConstraintsSatisfied() { + return mAllConstraintsSatisfied; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + byte flg = 0; + if (mAllConstraintsSatisfied) flg |= 0x1; + dest.writeByte(flg); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ InstallConstraintsResult(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + byte flg = in.readByte(); + boolean allConstraintsSatisfied = (flg & 0x1) != 0; + + this.mAllConstraintsSatisfied = allConstraintsSatisfied; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<InstallConstraintsResult> CREATOR + = new Parcelable.Creator<InstallConstraintsResult>() { + @Override + public InstallConstraintsResult[] newArray(int size) { + return new InstallConstraintsResult[size]; + } + + @Override + public InstallConstraintsResult createFromParcel(@NonNull Parcel in) { + return new InstallConstraintsResult(in); + } + }; + + @DataClass.Generated( + time = 1668650523745L, + codegenVersion = "1.0.23", + sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java", + inputSignatures = "private boolean mAllConstraintsSatisfied\nclass InstallConstraintsResult extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + + } + + /** + * A class to encapsulate constraints for installation. + * + * When used with {@link #checkInstallConstraints(List, InstallConstraints, Consumer)}, it + * specifies the conditions to check against for the packages in question. This can be used + * by app stores to deliver auto updates without disrupting the user experience (referred as + * gentle update) - for example, an app store might hold off updates when it find out the + * app to update is interacting with the user. + * + * Use {@link Builder} to create a new instance and call mutator methods to add constraints. + * If no mutators were called, default constraints will be generated which implies no + * constraints. It is recommended to use preset constraints which are useful in most + * cases. + * + * For the purpose of gentle update, it is recommended to always use {@link #GENTLE_UPDATE} + * for the system knows best how to do it. It will also benefits the installer as the + * platform evolves and add more constraints to improve the accuracy and efficiency of + * gentle update. + * + * Note the constraints are applied transitively. If app Foo is used by app Bar (via shared + * library or bounded service), the constraints will also be applied to Bar. + */ + @DataClass(genParcelable = true, genHiddenConstructor = true) + public static final class InstallConstraints implements Parcelable { + /** + * Preset constraints suitable for gentle update. + */ + @NonNull + public static final InstallConstraints GENTLE_UPDATE = + new Builder().requireAppNotInteracting().build(); + + private final boolean mRequireDeviceIdle; + private final boolean mRequireAppNotForeground; + private final boolean mRequireAppNotInteracting; + private final boolean mRequireAppNotTopVisible; + private final boolean mRequireNotInCall; + + /** + * Builder class for constructing {@link InstallConstraints}. + */ + public static final class Builder { + private boolean mRequireDeviceIdle; + private boolean mRequireAppNotForeground; + private boolean mRequireAppNotInteracting; + private boolean mRequireAppNotTopVisible; + private boolean mRequireNotInCall; + + /** + * This constraint requires the device is idle. + */ + @SuppressLint("BuilderSetStyle") + @NonNull + public Builder requireDeviceIdle() { + mRequireDeviceIdle = true; + return this; + } + + /** + * This constraint requires the app in question is not in the foreground. + */ + @SuppressLint("BuilderSetStyle") + @NonNull + public Builder requireAppNotForeground() { + mRequireAppNotForeground = true; + return this; + } + + /** + * This constraint requires the app in question is not interacting with the user. + * User interaction includes: + * <ul> + * <li>playing or recording audio/video</li> + * <li>sending or receiving network data</li> + * <li>being visible to the user</li> + * </ul> + */ + @SuppressLint("BuilderSetStyle") + @NonNull + public Builder requireAppNotInteracting() { + mRequireAppNotInteracting = true; + return this; + } + + /** + * This constraint requires the app in question is not top-visible to the user. + * A top-visible app is showing UI at the top of the screen that the user is + * interacting with. + * + * Note this constraint is a subset of {@link #requireAppNotForeground()} + * because a top-visible app is also a foreground app. This is also a subset + * of {@link #requireAppNotInteracting()} because a top-visible app is interacting + * with the user. + */ + @SuppressLint("BuilderSetStyle") + @NonNull + public Builder requireAppNotTopVisible() { + mRequireAppNotTopVisible = true; + return this; + } + + /** + * This constraint requires there is no ongoing call in the device. + */ + @SuppressLint("BuilderSetStyle") + @NonNull + public Builder requireNotInCall() { + mRequireNotInCall = true; + return this; + } + + /** + * Builds a new {@link InstallConstraints} instance. + */ + @NonNull + public InstallConstraints build() { + return new InstallConstraints(mRequireDeviceIdle, mRequireAppNotForeground, + mRequireAppNotInteracting, mRequireAppNotTopVisible, mRequireNotInCall); + } + } + + + + // Code below generated by codegen v1.0.23. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/content/pm/PackageInstaller.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + /** + * Creates a new InstallConstraints. + * + * @hide + */ + @DataClass.Generated.Member + public InstallConstraints( + boolean requireDeviceIdle, + boolean requireAppNotForeground, + boolean requireAppNotInteracting, + boolean requireAppNotTopVisible, + boolean requireNotInCall) { + this.mRequireDeviceIdle = requireDeviceIdle; + this.mRequireAppNotForeground = requireAppNotForeground; + this.mRequireAppNotInteracting = requireAppNotInteracting; + this.mRequireAppNotTopVisible = requireAppNotTopVisible; + this.mRequireNotInCall = requireNotInCall; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public boolean isRequireDeviceIdle() { + return mRequireDeviceIdle; + } + + @DataClass.Generated.Member + public boolean isRequireAppNotForeground() { + return mRequireAppNotForeground; + } + + @DataClass.Generated.Member + public boolean isRequireAppNotInteracting() { + return mRequireAppNotInteracting; + } + + @DataClass.Generated.Member + public boolean isRequireAppNotTopVisible() { + return mRequireAppNotTopVisible; + } + + @DataClass.Generated.Member + public boolean isRequireNotInCall() { + return mRequireNotInCall; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + byte flg = 0; + if (mRequireDeviceIdle) flg |= 0x1; + if (mRequireAppNotForeground) flg |= 0x2; + if (mRequireAppNotInteracting) flg |= 0x4; + if (mRequireAppNotTopVisible) flg |= 0x8; + if (mRequireNotInCall) flg |= 0x10; + dest.writeByte(flg); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ InstallConstraints(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + byte flg = in.readByte(); + boolean requireDeviceIdle = (flg & 0x1) != 0; + boolean requireAppNotForeground = (flg & 0x2) != 0; + boolean requireAppNotInteracting = (flg & 0x4) != 0; + boolean requireAppNotTopVisible = (flg & 0x8) != 0; + boolean requireNotInCall = (flg & 0x10) != 0; + + this.mRequireDeviceIdle = requireDeviceIdle; + this.mRequireAppNotForeground = requireAppNotForeground; + this.mRequireAppNotInteracting = requireAppNotInteracting; + this.mRequireAppNotTopVisible = requireAppNotTopVisible; + this.mRequireNotInCall = requireNotInCall; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<InstallConstraints> CREATOR + = new Parcelable.Creator<InstallConstraints>() { + @Override + public InstallConstraints[] newArray(int size) { + return new InstallConstraints[size]; + } + + @Override + public InstallConstraints createFromParcel(@NonNull Parcel in) { + return new InstallConstraints(in); + } + }; + + @DataClass.Generated( + time = 1668650523752L, + codegenVersion = "1.0.23", + sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java", + inputSignatures = "public static final @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints GENTLE_UPDATE\nprivate final boolean mRequireDeviceIdle\nprivate final boolean mRequireAppNotForeground\nprivate final boolean mRequireAppNotInteracting\nprivate final boolean mRequireAppNotTopVisible\nprivate final boolean mRequireNotInCall\nclass InstallConstraints extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mRequireDeviceIdle\nprivate boolean mRequireAppNotForeground\nprivate boolean mRequireAppNotInteracting\nprivate boolean mRequireAppNotTopVisible\nprivate boolean mRequireNotInCall\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireDeviceIdle()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotForeground()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotInteracting()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotTopVisible()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireNotInCall()\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints build()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + + } + } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index ec43ae3b43cb..1961fa166804 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7284,6 +7284,10 @@ android:permission="android.permission.BIND_JOB_SERVICE" > </service> + <service android:name="com.android.server.pm.GentleUpdateHelper$Service" + android:permission="android.permission.BIND_JOB_SERVICE" > + </service> + <service android:name="com.android.server.autofill.AutofillCompatAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" diff --git a/services/core/java/com/android/server/pm/AppStateHelper.java b/services/core/java/com/android/server/pm/AppStateHelper.java new file mode 100644 index 000000000000..9ea350f265b6 --- /dev/null +++ b/services/core/java/com/android/server/pm/AppStateHelper.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2022 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.app.ActivityManager; +import android.app.ActivityManager.RunningAppProcessInfo; +import android.content.Context; +import android.media.IAudioService; +import android.os.ServiceManager; +import android.text.TextUtils; +import android.util.ArraySet; + +import com.android.internal.util.ArrayUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * A helper class to provide queries for app states concerning gentle-update. + */ +public class AppStateHelper { + private final Context mContext; + + public AppStateHelper(Context context) { + mContext = context; + } + + /** + * True if the package is loaded into the process. + */ + private static boolean isPackageLoaded(RunningAppProcessInfo info, String packageName) { + return ArrayUtils.contains(info.pkgList, packageName) + || ArrayUtils.contains(info.pkgDeps, packageName); + } + + /** + * Returns the importance of the given package. + */ + private int getImportance(String packageName) { + var am = mContext.getSystemService(ActivityManager.class); + return am.getPackageImportance(packageName); + } + + /** + * True if the app owns the audio focus. + */ + private boolean hasAudioFocus(String packageName) { + var audioService = IAudioService.Stub.asInterface( + ServiceManager.getService(Context.AUDIO_SERVICE)); + try { + var focusInfos = audioService.getFocusStack(); + int size = focusInfos.size(); + var audioFocusPackage = (size > 0) ? focusInfos.get(size - 1).getPackageName() : null; + return TextUtils.equals(packageName, audioFocusPackage); + } catch (Exception ignore) { + } + return false; + } + + /** + * True if the app is in the foreground. + */ + private boolean isAppForeground(String packageName) { + return getImportance(packageName) <= RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE; + } + + /** + * True if the app is currently at the top of the screen that the user is interacting with. + */ + public boolean isAppTopVisible(String packageName) { + return getImportance(packageName) <= RunningAppProcessInfo.IMPORTANCE_FOREGROUND; + } + + /** + * True if the app is playing/recording audio. + */ + private boolean hasActiveAudio(String packageName) { + // TODO(b/235306967): also check recording + return hasAudioFocus(packageName); + } + + /** + * True if the app is sending or receiving network data. + */ + private boolean hasActiveNetwork(String packageName) { + // To be implemented + return false; + } + + /** + * True if any app is interacting with the user. + */ + public boolean hasInteractingApp(List<String> packageNames) { + for (var packageName : packageNames) { + if (hasActiveAudio(packageName) + || hasActiveNetwork(packageName) + || isAppTopVisible(packageName)) { + return true; + } + } + return false; + } + + /** + * True if any app is in the foreground. + */ + public boolean hasForegroundApp(List<String> packageNames) { + for (var packageName : packageNames) { + if (isAppForeground(packageName)) { + return true; + } + } + return false; + } + + /** + * True if any app is top visible. + */ + public boolean hasTopVisibleApp(List<String> packageNames) { + for (var packageName : packageNames) { + if (isAppTopVisible(packageName)) { + return true; + } + } + return false; + } + + /** + * True if there is an ongoing phone call. + */ + public boolean isInCall() { + // To be implemented + return false; + } + + /** + * Returns a list of packages which depend on {@code packageNames}. These are the packages + * that will be affected when updating {@code packageNames} and should participate in + * the evaluation of install constraints. + * + * TODO(b/235306967): Also include bounded services as dependency. + */ + public List<String> getDependencyPackages(List<String> packageNames) { + var results = new ArraySet<String>(); + var am = mContext.getSystemService(ActivityManager.class); + for (var info : am.getRunningAppProcesses()) { + for (var packageName : packageNames) { + if (!isPackageLoaded(info, packageName)) { + continue; + } + for (var pkg : info.pkgList) { + results.add(pkg); + } + } + } + return new ArrayList<>(results); + } +} diff --git a/services/core/java/com/android/server/pm/GentleUpdateHelper.java b/services/core/java/com/android/server/pm/GentleUpdateHelper.java new file mode 100644 index 000000000000..247ac9095bff --- /dev/null +++ b/services/core/java/com/android/server/pm/GentleUpdateHelper.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2022 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.annotation.WorkerThread; +import android.app.ActivityThread; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageInstaller.InstallConstraints; +import android.content.pm.PackageInstaller.InstallConstraintsResult; +import android.os.Handler; +import android.os.Looper; +import android.util.Slog; + +import java.util.ArrayDeque; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * A helper class to coordinate install flow for sessions with install constraints. + * These sessions will be pending and wait until the constraints are satisfied to + * resume installation. + */ +public class GentleUpdateHelper { + private static final String TAG = "GentleUpdateHelper"; + private static final int JOB_ID = 235306967; // bug id + // The timeout used to determine whether the device is idle or not. + private static final long PENDING_CHECK_MILLIS = TimeUnit.SECONDS.toMillis(10); + + /** + * A wrapper class used by JobScheduler to schedule jobs. + */ + public static class Service extends JobService { + @Override + public boolean onStartJob(JobParameters params) { + try { + var pis = (PackageInstallerService) ActivityThread.getPackageManager() + .getPackageInstaller(); + var helper = pis.getGentleUpdateHelper(); + helper.mHandler.post(helper::runIdleJob); + } catch (Exception e) { + Slog.e(TAG, "Failed to get PackageInstallerService", e); + } + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } + } + + private static class PendingInstallConstraintsCheck { + public final List<String> packageNames; + public final InstallConstraints constraints; + public final CompletableFuture<InstallConstraintsResult> future; + PendingInstallConstraintsCheck(List<String> packageNames, + InstallConstraints constraints, + CompletableFuture<InstallConstraintsResult> future) { + this.packageNames = packageNames; + this.constraints = constraints; + this.future = future; + } + } + + private final Context mContext; + private final Handler mHandler; + private final AppStateHelper mAppStateHelper; + // Worker thread only + private final ArrayDeque<PendingInstallConstraintsCheck> mPendingChecks = new ArrayDeque<>(); + private boolean mHasPendingIdleJob; + + GentleUpdateHelper(Context context, Looper looper, AppStateHelper appStateHelper) { + mContext = context; + mHandler = new Handler(looper); + mAppStateHelper = appStateHelper; + } + + /** + * Checks if install constraints are satisfied for the given packages. + */ + CompletableFuture<InstallConstraintsResult> checkInstallConstraints( + List<String> packageNames, InstallConstraints constraints) { + var future = new CompletableFuture<InstallConstraintsResult>(); + mHandler.post(() -> { + var pendingCheck = new PendingInstallConstraintsCheck( + packageNames, constraints, future); + if (constraints.isRequireDeviceIdle()) { + mPendingChecks.add(pendingCheck); + // JobScheduler doesn't provide queries about whether the device is idle. + // We schedule 2 tasks to determine device idle. If the idle job is executed + // before the delayed runnable, we know the device is idle. + // Note #processPendingCheck will be no-op for the task executed later. + scheduleIdleJob(); + mHandler.postDelayed(() -> processPendingCheck(pendingCheck, false), + PENDING_CHECK_MILLIS); + } else { + processPendingCheck(pendingCheck, false); + } + }); + return future; + } + + @WorkerThread + private void scheduleIdleJob() { + if (mHasPendingIdleJob) { + // No need to schedule the job again + return; + } + mHasPendingIdleJob = true; + var componentName = new ComponentName( + mContext.getPackageName(), GentleUpdateHelper.Service.class.getName()); + var jobInfo = new JobInfo.Builder(JOB_ID, componentName) + .setRequiresDeviceIdle(true) + .build(); + var jobScheduler = mContext.getSystemService(JobScheduler.class); + jobScheduler.schedule(jobInfo); + } + + @WorkerThread + private void runIdleJob() { + mHasPendingIdleJob = false; + processPendingChecksInIdle(); + } + + @WorkerThread + private void processPendingCheck(PendingInstallConstraintsCheck pendingCheck, boolean isIdle) { + var future = pendingCheck.future; + if (future.isDone()) { + return; + } + var constraints = pendingCheck.constraints; + var packageNames = mAppStateHelper.getDependencyPackages(pendingCheck.packageNames); + var constraintsSatisfied = (!constraints.isRequireDeviceIdle() || isIdle) + && (!constraints.isRequireAppNotForeground() + || !mAppStateHelper.hasForegroundApp(packageNames)) + && (!constraints.isRequireAppNotInteracting() + || !mAppStateHelper.hasInteractingApp(packageNames)) + && (!constraints.isRequireAppNotTopVisible() + || !mAppStateHelper.hasTopVisibleApp(packageNames)) + && (!constraints.isRequireNotInCall() + || !mAppStateHelper.isInCall()); + future.complete(new InstallConstraintsResult((constraintsSatisfied))); + } + + @WorkerThread + private void processPendingChecksInIdle() { + while (!mPendingChecks.isEmpty()) { + processPendingCheck(mPendingChecks.remove(), true); + } + } +} diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 653a882b3447..409d3524c312 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -44,6 +44,7 @@ import android.content.pm.IPackageInstallerCallback; import android.content.pm.IPackageInstallerSession; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; +import android.content.pm.PackageInstaller.InstallConstraints; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageInstaller.SessionParams; import android.content.pm.PackageItemInfo; @@ -54,12 +55,14 @@ import android.graphics.Bitmap; import android.net.Uri; import android.os.Binder; import android.os.Build; +import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.Process; +import android.os.RemoteCallback; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.SELinux; @@ -88,6 +91,7 @@ import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.util.ImageUtils; import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.Preconditions; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.IoThread; @@ -186,6 +190,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements private final InternalCallback mInternalCallback = new InternalCallback(); private final PackageSessionVerifier mSessionVerifier; + private final GentleUpdateHelper mGentleUpdateHelper; /** * Used for generating session IDs. Since this is created at boot time, @@ -272,6 +277,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements mStagingManager = new StagingManager(context); mSessionVerifier = new PackageSessionVerifier(context, mPm, mApexManager, apexParserSupplier, mInstallThread.getLooper()); + mGentleUpdateHelper = new GentleUpdateHelper( + context, mInstallThread.getLooper(), new AppStateHelper(context)); LocalServices.getService(SystemServiceManager.class).startService( new Lifecycle(context, this)); @@ -1233,6 +1240,33 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } @Override + public void checkInstallConstraints(String installerPackageName, List<String> packageNames, + InstallConstraints constraints, RemoteCallback callback) { + Preconditions.checkArgument(packageNames != null); + Preconditions.checkArgument(constraints != null); + Preconditions.checkArgument(callback != null); + + final var snapshot = mPm.snapshotComputer(); + final int callingUid = Binder.getCallingUid(); + if (!isCalledBySystemOrShell(callingUid)) { + for (var packageName : packageNames) { + var ps = snapshot.getPackageStateInternal(packageName); + if (ps == null || !TextUtils.equals( + ps.getInstallSource().mInstallerPackageName, installerPackageName)) { + throw new SecurityException("Caller has no access to package " + packageName); + } + } + } + + var future = mGentleUpdateHelper.checkInstallConstraints(packageNames, constraints); + future.thenAccept(result -> { + var b = new Bundle(); + b.putParcelable("result", result); + callback.sendResult(b); + }); + } + + @Override public void registerCallback(IPackageInstallerCallback callback, int userId) { final Computer snapshot = mPm.snapshotComputer(); snapshot.enforceCrossUserPermission(Binder.getCallingUid(), userId, true, false, @@ -1265,6 +1299,11 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } @Override + public GentleUpdateHelper getGentleUpdateHelper() { + return mGentleUpdateHelper; + } + + @Override public void bypassNextStagedInstallerCheck(boolean value) { if (!isCalledBySystemOrShell(Binder.getCallingUid())) { throw new SecurityException("Caller not allowed to bypass staged installer check"); diff --git a/services/core/java/com/android/server/pm/PackageSessionProvider.java b/services/core/java/com/android/server/pm/PackageSessionProvider.java index ad5cf1341b2a..79b88b38364d 100644 --- a/services/core/java/com/android/server/pm/PackageSessionProvider.java +++ b/services/core/java/com/android/server/pm/PackageSessionProvider.java @@ -29,4 +29,9 @@ public interface PackageSessionProvider { PackageInstallerSession getSession(int sessionId); PackageSessionVerifier getSessionVerifier(); + + /** + * Get the GentleUpdateHelper instance. + */ + GentleUpdateHelper getGentleUpdateHelper(); } |