Install constraints API

Design doc: go/installconstraints

Install constraints are the requirements for a session
to proceed installation. The API can be used by app stores
to deliver auto-updates without disrupting the user
experience.

This CL addes PackageInstaller#checkInstallConstraints
which can be used by installers to check whether
constraints are satisfied before installing a package.

Bug: 235306967
Test: atest CtsStagedInstallHostTestCases
Change-Id: I3b66a3bf5b6f9411bdb3ea27d6c4ff11a53a17e0
This commit is contained in:
JW Wang
2022-09-14 16:18:34 +08:00
parent c1861f634a
commit 1c61ab5cb2
9 changed files with 809 additions and 0 deletions

View File

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

View File

@@ -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);
}

View File

@@ -16,6 +16,7 @@
package android.content.pm;
parcelable PackageInstaller.InstallConstraints;
parcelable PackageInstaller.SessionParams;
parcelable PackageInstaller.SessionInfo;
parcelable PackageInstaller.PreapprovalDetails;

View File

@@ -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
@@ -853,6 +856,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>
@@ -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
}
}

View File

@@ -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"

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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));
@@ -1232,6 +1239,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();
@@ -1264,6 +1298,11 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements
return mSessionVerifier;
}
@Override
public GentleUpdateHelper getGentleUpdateHelper() {
return mGentleUpdateHelper;
}
@Override
public void bypassNextStagedInstallerCheck(boolean value) {
if (!isCalledBySystemOrShell(Binder.getCallingUid())) {

View File

@@ -29,4 +29,9 @@ public interface PackageSessionProvider {
PackageInstallerSession getSession(int sessionId);
PackageSessionVerifier getSessionVerifier();
/**
* Get the GentleUpdateHelper instance.
*/
GentleUpdateHelper getGentleUpdateHelper();
}