summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt30
-rw-r--r--core/java/android/content/pm/IPackageInstaller.aidl3
-rw-r--r--core/java/android/content/pm/PackageInstaller.aidl1
-rw-r--r--core/java/android/content/pm/PackageInstaller.java384
-rw-r--r--core/res/AndroidManifest.xml4
-rw-r--r--services/core/java/com/android/server/pm/AppStateHelper.java172
-rw-r--r--services/core/java/com/android/server/pm/GentleUpdateHelper.java171
-rw-r--r--services/core/java/com/android/server/pm/PackageInstallerService.java39
-rw-r--r--services/core/java/com/android/server/pm/PackageSessionProvider.java5
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();
}