From f8d1c26e50940390d607f4ad7f27cf50aa4a1b11 Mon Sep 17 00:00:00 2001 From: JW Wang Date: Fri, 9 Dec 2022 08:32:50 +0800 Subject: [PATCH 1/2] Improve the logic of checking device idle * #checkDeviceIdle encapsulates the logic of checking device idle, making the life of the caller easier. * Now timeout-check always happens after device-idle-check. No clamping is needed w.r.t PENDING_CHECK_MILLIS. Bug: 235306967 Test: atest CtsStagedInstallHostTestCases Change-Id: Ibbdd34518ce9f53b5053eff18826e2dd72edac6b --- .../android/content/pm/PackageInstaller.java | 3 + .../android/server/pm/GentleUpdateHelper.java | 78 ++++++++++++------- 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index f17d8fac9ede5..d8dae6d87de25 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -906,6 +906,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. + *

+ * 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: diff --git a/services/core/java/com/android/server/pm/GentleUpdateHelper.java b/services/core/java/com/android/server/pm/GentleUpdateHelper.java index 9ad847d5d88cf..ae80babac0c6a 100644 --- a/services/core/java/com/android/server/pm/GentleUpdateHelper.java +++ b/services/core/java/com/android/server/pm/GentleUpdateHelper.java @@ -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 mPendingChecks = new ArrayDeque<>(); + private final ArrayList> mPendingIdleFutures = new ArrayList<>(); private boolean mHasPendingIdleJob; GentleUpdateHelper(Context context, Looper looper, AppStateHelper appStateHelper) { @@ -130,39 +141,41 @@ public class GentleUpdateHelper { CompletableFuture checkInstallConstraints( List packageNames, InstallConstraints constraints, long timeoutMillis) { - var future = new CompletableFuture(); + var resultFuture = new CompletableFuture(); 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 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(); + 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 From d2f1260c58633a6808e03c84a9a001fef3ce88b1 Mon Sep 17 00:00:00 2001 From: JW Wang Date: Mon, 12 Dec 2022 10:50:02 +0800 Subject: [PATCH 2/2] Implement AppStateHelper#hasActiveNetwork An app is said to be network active if it has sent or received network data over the pas 10 minutes. For now we monitor only WIFI and mobile network. This should cover most cases of network usage. Bug: 235306967 Test: atest CtsStagedInstallHostTestCases Change-Id: Iac49bd83202932fc7a411cdc8a97e0a949c3f691 --- .../com/android/server/pm/AppStateHelper.java | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/services/core/java/com/android/server/pm/AppStateHelper.java b/services/core/java/com/android/server/pm/AppStateHelper.java index bb0c41ea5591e..2ef193cacbaea 100644 --- a/services/core/java/com/android/server/pm/AppStateHelper.java +++ b/services/core/java/com/android/server/pm/AppStateHelper.java @@ -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 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 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 packageNames) { for (var packageName : packageNames) { if (hasActiveAudio(packageName) - || hasActiveNetwork(packageName) || isAppTopVisible(packageName)) { return true; } } - return false; + return hasActiveNetwork(packageNames); } /**