Merge changes from topic "install_constraints"

* changes:
  Implement AppStateHelper#hasActiveNetwork
  Improve the logic of checking device idle
This commit is contained in:
Chun-Wei Wang
2022-12-19 00:58:49 +00:00
committed by Android (Google) Code Review
3 changed files with 88 additions and 37 deletions

View File

@@ -907,6 +907,9 @@ public class PackageInstaller {
/**
* Similar to {@link #checkInstallConstraints(List, InstallConstraints, Executor, Consumer)},
* but the callback is invoked only when the constraints are satisfied or after timeout.
* <p>
* Note: the device idle constraint might take a long time to evaluate. The system will
* ensure the constraint is evaluated completely before handling timeout.
*
* @param callback Called when the constraints are satisfied or after timeout.
* Intents sent to this callback contain:

View File

@@ -19,9 +19,13 @@ package com.android.server.pm;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.ActivityManagerInternal;
import android.app.ActivityThread;
import android.app.usage.NetworkStats;
import android.app.usage.NetworkStatsManager;
import android.content.Context;
import android.media.AudioManager;
import android.media.IAudioService;
import android.net.ConnectivityManager;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.telecom.TelecomManager;
@@ -33,11 +37,15 @@ import com.android.server.LocalServices;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* A helper class to provide queries for app states concerning gentle-update.
*/
public class AppStateHelper {
// The duration to monitor network usage to determine if network is active or not
private static final long ACTIVE_NETWORK_DURATION_MILLIS = TimeUnit.MINUTES.toMillis(10);
private final Context mContext;
public AppStateHelper(Context context) {
@@ -127,26 +135,48 @@ public class AppStateHelper {
return hasAudioFocus(packageName) || isRecordingAudio(packageName);
}
/**
* True if the app is sending or receiving network data.
*/
private boolean hasActiveNetwork(String packageName) {
// To be implemented
private boolean hasActiveNetwork(List<String> packageNames, int networkType) {
var pm = ActivityThread.getPackageManager();
var nsm = mContext.getSystemService(NetworkStatsManager.class);
var endTime = System.currentTimeMillis();
var startTime = endTime - ACTIVE_NETWORK_DURATION_MILLIS;
try (var stats = nsm.querySummary(networkType, null, startTime, endTime)) {
var bucket = new NetworkStats.Bucket();
while (stats.hasNextBucket()) {
stats.getNextBucket(bucket);
var packageName = pm.getNameForUid(bucket.getUid());
if (!packageNames.contains(packageName)) {
continue;
}
if (bucket.getRxPackets() > 0 || bucket.getTxPackets() > 0) {
return true;
}
}
} catch (Exception ignore) {
}
return false;
}
/**
* True if any app has sent or received network data over the past
* {@link #ACTIVE_NETWORK_DURATION_MILLIS} milliseconds.
*/
private boolean hasActiveNetwork(List<String> packageNames) {
return hasActiveNetwork(packageNames, ConnectivityManager.TYPE_WIFI)
|| hasActiveNetwork(packageNames, ConnectivityManager.TYPE_MOBILE);
}
/**
* 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;
return hasActiveNetwork(packageNames);
}
/**

View File

@@ -39,7 +39,10 @@ import android.os.SystemProperties;
import android.text.format.DateUtils;
import android.util.Slog;
import com.android.internal.util.Preconditions;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@@ -101,6 +104,13 @@ public class GentleUpdateHelper {
public boolean isTimedOut() {
return SystemClock.elapsedRealtime() >= mFinishTime;
}
/**
* The remaining time before this pending check is timed out.
*/
public long getRemainingTimeMillis() {
long timeout = mFinishTime - SystemClock.elapsedRealtime();
return Math.max(timeout, 0);
}
}
private final Context mContext;
@@ -108,6 +118,7 @@ public class GentleUpdateHelper {
private final AppStateHelper mAppStateHelper;
// Worker thread only
private final ArrayDeque<PendingInstallConstraintsCheck> mPendingChecks = new ArrayDeque<>();
private final ArrayList<CompletableFuture<Boolean>> mPendingIdleFutures = new ArrayList<>();
private boolean mHasPendingIdleJob;
GentleUpdateHelper(Context context, Looper looper, AppStateHelper appStateHelper) {
@@ -130,39 +141,41 @@ public class GentleUpdateHelper {
CompletableFuture<InstallConstraintsResult> checkInstallConstraints(
List<String> packageNames, InstallConstraints constraints,
long timeoutMillis) {
var future = new CompletableFuture<InstallConstraintsResult>();
var resultFuture = new CompletableFuture<InstallConstraintsResult>();
mHandler.post(() -> {
long clampedTimeoutMillis = timeoutMillis;
if (constraints.isRequireDeviceIdle()) {
// Device-idle-constraint is required. Clamp the timeout to ensure
// timeout-check happens after device-idle-check.
clampedTimeoutMillis = Math.max(timeoutMillis, PENDING_CHECK_MILLIS);
}
var pendingCheck = new PendingInstallConstraintsCheck(
packageNames, constraints, future, clampedTimeoutMillis);
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 if (!processPendingCheck(pendingCheck, false)) {
// Not resolved. Schedule a job for re-check
mPendingChecks.add(pendingCheck);
scheduleIdleJob();
}
if (!future.isDone()) {
// Ensure the pending check is resolved after timeout, no matter constraints
// satisfied or not.
mHandler.postDelayed(() -> processPendingCheck(pendingCheck, false),
clampedTimeoutMillis);
}
packageNames, constraints, resultFuture, timeoutMillis);
var deviceIdleFuture = constraints.isRequireDeviceIdle()
? checkDeviceIdle() : CompletableFuture.completedFuture(false);
deviceIdleFuture.thenAccept(isIdle -> {
Preconditions.checkState(mHandler.getLooper().isCurrentThread());
if (!processPendingCheck(pendingCheck, isIdle)) {
// Not resolved. Schedule a job for re-check
mPendingChecks.add(pendingCheck);
scheduleIdleJob();
// Ensure the pending check is resolved after timeout, no matter constraints
// satisfied or not.
mHandler.postDelayed(() -> processPendingCheck(
pendingCheck, false), pendingCheck.getRemainingTimeMillis());
}
});
});
return resultFuture;
}
/**
* Checks if the device is idle or not.
* @return A future resolved to {@code true} if the device is idle, or {@code false} if not.
*/
@WorkerThread
private CompletableFuture<Boolean> checkDeviceIdle() {
// JobScheduler doesn't provide queries about whether the device is idle.
// We schedule 2 tasks here and the task which resolves
// the future first will determine whether the device is idle or not.
var future = new CompletableFuture<Boolean>();
mPendingIdleFutures.add(future);
scheduleIdleJob();
mHandler.postDelayed(() -> future.complete(false), PENDING_CHECK_MILLIS);
return future;
}
@@ -194,6 +207,11 @@ public class GentleUpdateHelper {
private void runIdleJob() {
mHasPendingIdleJob = false;
processPendingChecksInIdle();
for (var f : mPendingIdleFutures) {
f.complete(true);
}
mPendingIdleFutures.clear();
}
@WorkerThread