Add explicit health check to PackageWatchdog
PackageWatchdog now uses the ExplicitHealthCheckController introduced in Ia030671c99699bd8d8273f32a97a1d3b7b015d3b when observing packages. Bug: 120598832 Test: Manually tested that after an APEX update, the network stack does not pass the explicit health check until WiFi is connected successfully. If Wi-Fi is never connected and the network stack monitoring duration is exceeded, the update is rolled back. Change-Id: I75d3cc909cabb4a4eb34df1d5022d1afc629dac3
This commit is contained in:
@@ -39,7 +39,9 @@ import android.text.TextUtils;
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -50,10 +52,22 @@ class ExplicitHealthCheckController {
|
||||
private static final String TAG = "ExplicitHealthCheckController";
|
||||
private final Object mLock = new Object();
|
||||
private final Context mContext;
|
||||
@GuardedBy("mLock") @Nullable private StateCallback mStateCallback;
|
||||
|
||||
// Called everytime the service is connected, so the watchdog can sync it's state with
|
||||
// the health check service. In practice, should never be null after it has been #setEnabled.
|
||||
@GuardedBy("mLock") @Nullable private Runnable mOnConnected;
|
||||
// Called everytime a package passes the health check, so the watchdog is notified of the
|
||||
// passing check. In practice, should never be null after it has been #setEnabled.
|
||||
@GuardedBy("mLock") @Nullable private Consumer<String> mPassedConsumer;
|
||||
// Actual binder object to the explicit health check service.
|
||||
@GuardedBy("mLock") @Nullable private IExplicitHealthCheckService mRemoteService;
|
||||
@GuardedBy("mLock") @Nullable private ServiceConnection mConnection;
|
||||
// Cache for packages supporting explicit health checks. This cache should not change while
|
||||
// the health check service is running.
|
||||
@GuardedBy("mLock") @Nullable private List<String> mSupportedPackages;
|
||||
// Connection to the explicit health check service, necessary to unbind
|
||||
@GuardedBy("mLock") @Nullable private ServiceConnection mConnection;
|
||||
// Bind state of the explicit health check service.
|
||||
@GuardedBy("mLock") private boolean mEnabled;
|
||||
|
||||
ExplicitHealthCheckController(Context context) {
|
||||
mContext = context;
|
||||
@@ -61,28 +75,40 @@ class ExplicitHealthCheckController {
|
||||
|
||||
/**
|
||||
* Requests an explicit health check for {@code packageName}.
|
||||
* After this request, the callback registered on {@link startService} can receive explicit
|
||||
* After this request, the callback registered on {@link #setCallbacks} can receive explicit
|
||||
* health check passed results.
|
||||
*
|
||||
* @throws IllegalStateException if the service is not started
|
||||
*/
|
||||
public void request(String packageName) throws RemoteException {
|
||||
synchronized (mLock) {
|
||||
if (!mEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
enforceServiceReadyLocked();
|
||||
|
||||
Slog.i(TAG, "Requesting health check for package " + packageName);
|
||||
mRemoteService.request(packageName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all explicit health checks for {@code packageName}.
|
||||
* After this request, the callback registered on {@link startService} can no longer receive
|
||||
* After this request, the callback registered on {@link #setCallbacks} can no longer receive
|
||||
* explicit health check passed results.
|
||||
*
|
||||
* @throws IllegalStateException if the service is not started
|
||||
*/
|
||||
public void cancel(String packageName) throws RemoteException {
|
||||
synchronized (mLock) {
|
||||
if (!mEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
enforceServiceReadyLocked();
|
||||
|
||||
Slog.i(TAG, "Cancelling health check for package " + packageName);
|
||||
mRemoteService.cancel(packageName);
|
||||
}
|
||||
}
|
||||
@@ -95,13 +121,21 @@ class ExplicitHealthCheckController {
|
||||
*/
|
||||
public void getSupportedPackages(Consumer<List<String>> consumer) throws RemoteException {
|
||||
synchronized (mLock) {
|
||||
if (!mEnabled) {
|
||||
consumer.accept(Collections.emptyList());
|
||||
return;
|
||||
}
|
||||
|
||||
enforceServiceReadyLocked();
|
||||
|
||||
if (mSupportedPackages == null) {
|
||||
Slog.d(TAG, "Getting health check supported packages");
|
||||
mRemoteService.getSupportedPackages(new RemoteCallback(result -> {
|
||||
mSupportedPackages = result.getStringArrayList(EXTRA_SUPPORTED_PACKAGES);
|
||||
consumer.accept(mSupportedPackages);
|
||||
}));
|
||||
} else {
|
||||
Slog.d(TAG, "Getting cached health check supported packages");
|
||||
consumer.accept(mSupportedPackages);
|
||||
}
|
||||
}
|
||||
@@ -115,95 +149,113 @@ class ExplicitHealthCheckController {
|
||||
*/
|
||||
public void getRequestedPackages(Consumer<List<String>> consumer) throws RemoteException {
|
||||
synchronized (mLock) {
|
||||
if (!mEnabled) {
|
||||
consumer.accept(Collections.emptyList());
|
||||
return;
|
||||
}
|
||||
|
||||
enforceServiceReadyLocked();
|
||||
|
||||
Slog.d(TAG, "Getting health check requested packages");
|
||||
mRemoteService.getRequestedPackages(new RemoteCallback(
|
||||
result -> consumer.accept(
|
||||
result.getStringArrayList(EXTRA_REQUESTED_PACKAGES))));
|
||||
}
|
||||
}
|
||||
|
||||
/** Enables or disables explicit health checks. */
|
||||
public void setEnabled(boolean enabled) {
|
||||
synchronized (mLock) {
|
||||
if (enabled == mEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Slog.i(TAG, "Setting explicit health checks enabled " + enabled);
|
||||
mEnabled = enabled;
|
||||
if (enabled) {
|
||||
bindService();
|
||||
} else {
|
||||
unbindService();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the explicit health check service.
|
||||
*
|
||||
* @param stateCallback will receive important state changes changes
|
||||
* @param passedConsumer will accept packages that pass explicit health checks
|
||||
*
|
||||
* @throws IllegalStateException if the service is already started
|
||||
* Sets callbacks to listen to important events from the controller.
|
||||
* Should be called at initialization.
|
||||
*/
|
||||
public void startService(StateCallback stateCallback, Consumer<String> passedConsumer) {
|
||||
public void setCallbacks(Runnable onConnected, Consumer<String> passedConsumer) {
|
||||
Preconditions.checkNotNull(onConnected);
|
||||
Preconditions.checkNotNull(passedConsumer);
|
||||
mOnConnected = onConnected;
|
||||
mPassedConsumer = passedConsumer;
|
||||
}
|
||||
|
||||
/** Binds to the explicit health check service. */
|
||||
private void bindService() {
|
||||
synchronized (mLock) {
|
||||
if (mRemoteService != null) {
|
||||
throw new IllegalStateException("Explicit health check service already started.");
|
||||
return;
|
||||
}
|
||||
mStateCallback = stateCallback;
|
||||
ComponentName component = getServiceComponentNameLocked();
|
||||
if (component == null) {
|
||||
Slog.wtf(TAG, "Explicit health check service not found");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setComponent(component);
|
||||
// TODO: Fix potential race conditions during mConnection state transitions.
|
||||
// E.g after #onServiceDisconected, the mRemoteService object is invalid until
|
||||
// we get an #onServiceConnected.
|
||||
mConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
synchronized (mLock) {
|
||||
mRemoteService = IExplicitHealthCheckService.Stub.asInterface(service);
|
||||
try {
|
||||
mRemoteService.setCallback(new RemoteCallback(result -> {
|
||||
String packageName =
|
||||
result.getString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE);
|
||||
if (!TextUtils.isEmpty(packageName)) {
|
||||
passedConsumer.accept(packageName);
|
||||
} else {
|
||||
Slog.w(TAG, "Empty package passed explicit health check?");
|
||||
}
|
||||
}));
|
||||
mStateCallback.onStart();
|
||||
Slog.i(TAG, "Explicit health check service is connected " + name);
|
||||
} catch (RemoteException e) {
|
||||
Slog.wtf(TAG, "Coud not setCallback on explicit health check service");
|
||||
}
|
||||
}
|
||||
initState(service);
|
||||
Slog.i(TAG, "Explicit health check service is connected " + name);
|
||||
}
|
||||
|
||||
@Override
|
||||
@MainThread
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
resetState();
|
||||
// Service crashed or process was killed, #onServiceConnected will be called.
|
||||
// Don't need to re-bind.
|
||||
Slog.i(TAG, "Explicit health check service is disconnected " + name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindingDied(ComponentName name) {
|
||||
resetState();
|
||||
// Application hosting service probably got updated
|
||||
// Need to re-bind.
|
||||
synchronized (mLock) {
|
||||
if (mEnabled) {
|
||||
unbindService();
|
||||
bindService();
|
||||
}
|
||||
}
|
||||
Slog.i(TAG, "Explicit health check service binding is dead " + name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNullBinding(ComponentName name) {
|
||||
resetState();
|
||||
Slog.i(TAG, "Explicit health check service binding is null " + name);
|
||||
// Should never happen. Service returned null from #onBind.
|
||||
Slog.wtf(TAG, "Explicit health check service binding is null?? " + name);
|
||||
}
|
||||
};
|
||||
|
||||
ComponentName component = getServiceComponentNameLocked();
|
||||
if (component != null) {
|
||||
Intent intent = new Intent();
|
||||
intent.setComponent(component);
|
||||
mContext.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE,
|
||||
UserHandle.of(UserHandle.USER_SYSTEM));
|
||||
}
|
||||
Slog.i(TAG, "Binding to explicit health service");
|
||||
mContext.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE,
|
||||
UserHandle.of(UserHandle.USER_SYSTEM));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Differentiate between expected vs unexpected stop?
|
||||
/** Callback to receive important {@link ExplicitHealthCheckController} state changes. */
|
||||
abstract static class StateCallback {
|
||||
/** The controller is ready and we can request explicit health checks for packages */
|
||||
public void onStart() {}
|
||||
|
||||
/** The controller is not ready and we cannot request explicit health checks for packages */
|
||||
public void onStop() {}
|
||||
}
|
||||
|
||||
/** Stops the explicit health check service. */
|
||||
public void stopService() {
|
||||
/** Unbinds the explicit health check service. */
|
||||
private void unbindService() {
|
||||
synchronized (mLock) {
|
||||
if (mRemoteService != null) {
|
||||
Slog.i(TAG, "Unbinding from explicit health service");
|
||||
mContext.unbindService(mConnection);
|
||||
mRemoteService = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,19 +299,41 @@ class ExplicitHealthCheckController {
|
||||
return name;
|
||||
}
|
||||
|
||||
private void resetState() {
|
||||
private void initState(IBinder service) {
|
||||
synchronized (mLock) {
|
||||
mStateCallback.onStop();
|
||||
mStateCallback = null;
|
||||
mSupportedPackages = null;
|
||||
mRemoteService = null;
|
||||
mConnection = null;
|
||||
mRemoteService = IExplicitHealthCheckService.Stub.asInterface(service);
|
||||
try {
|
||||
mRemoteService.setCallback(new RemoteCallback(result -> {
|
||||
String packageName = result.getString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE);
|
||||
if (!TextUtils.isEmpty(packageName)) {
|
||||
synchronized (mLock) {
|
||||
if (mPassedConsumer == null) {
|
||||
Slog.w(TAG, "Health check passed for package " + packageName
|
||||
+ "but no consumer registered.");
|
||||
} else {
|
||||
mPassedConsumer.accept(packageName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Slog.w(TAG, "Empty package passed explicit health check?");
|
||||
}
|
||||
}));
|
||||
if (mOnConnected == null) {
|
||||
Slog.w(TAG, "Health check service connected but no runnable registered.");
|
||||
} else {
|
||||
mOnConnected.run();
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Slog.wtf(TAG, "Could not setCallback on explicit health check service");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void enforceServiceReadyLocked() {
|
||||
if (mRemoteService == null) {
|
||||
// TODO: Try to bind to service
|
||||
throw new IllegalStateException("Explicit health check service not ready");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,12 @@ import android.content.pm.VersionedPackage;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.RemoteException;
|
||||
import android.os.SystemClock;
|
||||
import android.text.TextUtils;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.ArraySet;
|
||||
import android.util.AtomicFile;
|
||||
import android.util.Log;
|
||||
import android.util.Slog;
|
||||
import android.util.Xml;
|
||||
|
||||
@@ -54,10 +55,12 @@ import java.io.InputStream;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Monitors the health of packages on the system and notifies interested observers when packages
|
||||
@@ -84,10 +87,10 @@ public class PackageWatchdog {
|
||||
private final Object mLock = new Object();
|
||||
// System server context
|
||||
private final Context mContext;
|
||||
// Handler to run package cleanup runnables
|
||||
private final Handler mTimerHandler;
|
||||
// Handler for processing IO and observer actions
|
||||
private final Handler mWorkerHandler;
|
||||
// Handler to run short running tasks
|
||||
private final Handler mShortTaskHandler;
|
||||
// Handler for processing IO and long running tasks
|
||||
private final Handler mLongTaskHandler;
|
||||
// Contains (observer-name -> observer-handle) that have ever been registered from
|
||||
// previous boots. Observers with all packages expired are periodically pruned.
|
||||
// It is saved to disk on system shutdown and repouplated on startup so it survives reboots.
|
||||
@@ -97,6 +100,12 @@ public class PackageWatchdog {
|
||||
private final AtomicFile mPolicyFile;
|
||||
// Runnable to prune monitored packages that have expired
|
||||
private final Runnable mPackageCleanup;
|
||||
private final ExplicitHealthCheckController mHealthCheckController;
|
||||
// Flag to control whether explicit health checks are supported or not
|
||||
@GuardedBy("mLock")
|
||||
private boolean mIsHealthCheckEnabled = true;
|
||||
@GuardedBy("mLock")
|
||||
private boolean mIsPackagesReady;
|
||||
// Last SystemClock#uptimeMillis a package clean up was executed.
|
||||
// 0 if mPackageCleanup not running.
|
||||
private long mUptimeAtLastRescheduleMs;
|
||||
@@ -104,32 +113,30 @@ public class PackageWatchdog {
|
||||
// 0 if mPackageCleanup not running.
|
||||
private long mDurationAtLastReschedule;
|
||||
|
||||
// TODO(b/120598832): Remove redundant context param
|
||||
private PackageWatchdog(Context context) {
|
||||
mContext = context;
|
||||
mPolicyFile = new AtomicFile(new File(new File(Environment.getDataDirectory(), "system"),
|
||||
"package-watchdog.xml"));
|
||||
mTimerHandler = new Handler(Looper.myLooper());
|
||||
mWorkerHandler = BackgroundThread.getHandler();
|
||||
mPackageCleanup = this::rescheduleCleanup;
|
||||
loadFromFile();
|
||||
// Needs to be constructed inline
|
||||
this(context, new AtomicFile(
|
||||
new File(new File(Environment.getDataDirectory(), "system"),
|
||||
"package-watchdog.xml")),
|
||||
new Handler(Looper.myLooper()), BackgroundThread.getHandler(),
|
||||
new ExplicitHealthCheckController(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PackageWatchdog for testing that uses the same {@code looper} for all handlers
|
||||
* and creates package-watchdog.xml in an apps data directory.
|
||||
* Creates a PackageWatchdog that allows injecting dependencies.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
PackageWatchdog(Context context, Looper looper) {
|
||||
PackageWatchdog(Context context, AtomicFile policyFile, Handler shortTaskHandler,
|
||||
Handler longTaskHandler, ExplicitHealthCheckController controller) {
|
||||
mContext = context;
|
||||
mPolicyFile = new AtomicFile(new File(context.getFilesDir(), "package-watchdog.xml"));
|
||||
mTimerHandler = new Handler(looper);
|
||||
mWorkerHandler = mTimerHandler;
|
||||
mPolicyFile = policyFile;
|
||||
mShortTaskHandler = shortTaskHandler;
|
||||
mLongTaskHandler = longTaskHandler;
|
||||
mPackageCleanup = this::rescheduleCleanup;
|
||||
mHealthCheckController = controller;
|
||||
loadFromFile();
|
||||
}
|
||||
|
||||
|
||||
/** Creates or gets singleton instance of PackageWatchdog. */
|
||||
public static PackageWatchdog getInstance(Context context) {
|
||||
synchronized (PackageWatchdog.class) {
|
||||
@@ -140,6 +147,20 @@ public class PackageWatchdog {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called during boot to notify when packages are ready on the device so we can start
|
||||
* binding.
|
||||
*/
|
||||
public void onPackagesReady() {
|
||||
synchronized (mLock) {
|
||||
mIsPackagesReady = true;
|
||||
mHealthCheckController.setCallbacks(this::updateHealthChecks,
|
||||
packageName -> onHealthCheckPassed(packageName));
|
||||
// Controller is disabled at creation until here where we may enable it
|
||||
mHealthCheckController.setEnabled(mIsHealthCheckEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers {@code observer} to listen for package failures
|
||||
*
|
||||
@@ -163,40 +184,63 @@ public class PackageWatchdog {
|
||||
* Starts observing the health of the {@code packages} for {@code observer} and notifies
|
||||
* {@code observer} of any package failures within the monitoring duration.
|
||||
*
|
||||
* <p>If monitoring a package with {@code withExplicitHealthCheck}, at the end of the monitoring
|
||||
* duration if {@link #onExplicitHealthCheckPassed} was never called,
|
||||
* <p>If monitoring a package supporting explicit health check, at the end of the monitoring
|
||||
* duration if {@link #onHealthCheckPassed} was never called,
|
||||
* {@link PackageHealthObserver#execute} will be called as if the package failed.
|
||||
*
|
||||
* <p>If {@code observer} is already monitoring a package in {@code packageNames},
|
||||
* the monitoring window of that package will be reset to {@code durationMs} and the health
|
||||
* check state will be reset to a default depending on {@code withExplictHealthCheck}.
|
||||
* check state will be reset to a default depending on if the package is contained in
|
||||
* {@link mPackagesWithExplicitHealthCheckEnabled}.
|
||||
*
|
||||
* @throws IllegalArgumentException if {@code packageNames} is empty
|
||||
* or {@code durationMs} is less than 1
|
||||
*/
|
||||
public void startObservingHealth(PackageHealthObserver observer, List<String> packageNames,
|
||||
long durationMs, boolean withExplicitHealthCheck) {
|
||||
if (packageNames.isEmpty() || durationMs < 1) {
|
||||
public void startObservingHealth(PackageHealthObserver observer, List<String> packages,
|
||||
long durationMs) {
|
||||
if (packages.isEmpty() || durationMs < 1) {
|
||||
throw new IllegalArgumentException("Observation not started, no packages specified"
|
||||
+ "or invalid duration");
|
||||
}
|
||||
if (!mIsPackagesReady) {
|
||||
// TODO: Queue observation requests when packages are not ready
|
||||
Slog.w(TAG, "Attempt to observe when packages not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Slog.i(TAG, "Getting packages supporting explicit health check");
|
||||
mHealthCheckController.getSupportedPackages(supportedPackages ->
|
||||
startObservingInner(observer, packages, durationMs, supportedPackages));
|
||||
} catch (RemoteException e) {
|
||||
Slog.wtf(TAG, "Failed to fetch supported explicit health check packages");
|
||||
}
|
||||
}
|
||||
|
||||
private void startObservingInner(PackageHealthObserver observer,
|
||||
List<String> packageNames, long durationMs,
|
||||
List<String> healthCheckSupportedPackages) {
|
||||
Slog.i(TAG, "Start observing packages " + packageNames
|
||||
+ ". Explicit health check supported packages " + healthCheckSupportedPackages);
|
||||
List<MonitoredPackage> packages = new ArrayList<>();
|
||||
for (int i = 0; i < packageNames.size(); i++) {
|
||||
// When observing packages withExplicitHealthCheck,
|
||||
// MonitoredPackage#mHasExplicitHealthCheckPassed will be false initially.
|
||||
packages.add(new MonitoredPackage(packageNames.get(i), durationMs,
|
||||
!withExplicitHealthCheck));
|
||||
String packageName = packageNames.get(i);
|
||||
boolean shouldEnableHealthCheck = healthCheckSupportedPackages.contains(packageName);
|
||||
// If we should enable explicit health check for a package,
|
||||
// MonitoredPackage#mHasHealthCheckPassed will be false
|
||||
// until PackageWatchdog#onHealthCheckPassed
|
||||
packages.add(new MonitoredPackage(packageName, durationMs, !shouldEnableHealthCheck));
|
||||
}
|
||||
synchronized (mLock) {
|
||||
ObserverInternal oldObserver = mAllObservers.get(observer.getName());
|
||||
if (oldObserver == null) {
|
||||
Slog.d(TAG, observer.getName() + " started monitoring health of packages "
|
||||
+ packageNames);
|
||||
Slog.d(TAG, observer.getName() + " started monitoring health "
|
||||
+ "of packages " + packageNames);
|
||||
mAllObservers.put(observer.getName(),
|
||||
new ObserverInternal(observer.getName(), packages));
|
||||
} else {
|
||||
Slog.d(TAG, observer.getName() + " added the following packages to monitor "
|
||||
+ packageNames);
|
||||
Slog.d(TAG, observer.getName() + " added the following "
|
||||
+ "packages to monitor " + packageNames);
|
||||
oldObserver.updatePackages(packages);
|
||||
}
|
||||
}
|
||||
@@ -204,9 +248,97 @@ public class PackageWatchdog {
|
||||
// Always reschedule because we may need to expire packages
|
||||
// earlier than we are already scheduled for
|
||||
rescheduleCleanup();
|
||||
updateHealthChecks();
|
||||
saveToFileAsync();
|
||||
}
|
||||
|
||||
private void requestCheck(String packageName) {
|
||||
try {
|
||||
Slog.d(TAG, "Requesting explicit health check for " + packageName);
|
||||
mHealthCheckController.request(packageName);
|
||||
} catch (RemoteException e) {
|
||||
Slog.wtf(TAG, "Failed to request explicit health check for " + packageName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelCheck(String packageName) {
|
||||
try {
|
||||
Slog.d(TAG, "Cancelling explicit health check for " + packageName);
|
||||
mHealthCheckController.cancel(packageName);
|
||||
} catch (RemoteException e) {
|
||||
Slog.wtf(TAG, "Failed to cancel explicit health check for " + packageName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void actOnDifference(Collection<String> collection1, Collection<String> collection2,
|
||||
Consumer<String> action) {
|
||||
Iterator<String> iterator = collection1.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
String packageName = iterator.next();
|
||||
if (!collection2.contains(packageName)) {
|
||||
action.accept(packageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChecksInner(List<String> supportedPackages,
|
||||
List<String> previousRequestedPackages) {
|
||||
boolean shouldUpdateFile = false;
|
||||
|
||||
synchronized (mLock) {
|
||||
Slog.i(TAG, "Updating explicit health checks. Supported packages: " + supportedPackages
|
||||
+ ". Requested packages: " + previousRequestedPackages);
|
||||
Set<String> newRequestedPackages = new ArraySet<>();
|
||||
Iterator<ObserverInternal> oit = mAllObservers.values().iterator();
|
||||
while (oit.hasNext()) {
|
||||
ObserverInternal observer = oit.next();
|
||||
Iterator<MonitoredPackage> pit =
|
||||
observer.mPackages.values().iterator();
|
||||
while (pit.hasNext()) {
|
||||
MonitoredPackage monitoredPackage = pit.next();
|
||||
String packageName = monitoredPackage.mName;
|
||||
if (!monitoredPackage.mHasPassedHealthCheck) {
|
||||
if (supportedPackages.contains(packageName)) {
|
||||
newRequestedPackages.add(packageName);
|
||||
} else {
|
||||
shouldUpdateFile = true;
|
||||
monitoredPackage.mHasPassedHealthCheck = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Support ending the binding if newRequestedPackages is empty.
|
||||
// Will have to re-bind when we #startObservingHealth.
|
||||
|
||||
// Cancel packages no longer requested
|
||||
actOnDifference(previousRequestedPackages, newRequestedPackages, p -> cancelCheck(p));
|
||||
// Request packages not yet requested
|
||||
actOnDifference(newRequestedPackages, previousRequestedPackages, p -> requestCheck(p));
|
||||
}
|
||||
|
||||
if (shouldUpdateFile) {
|
||||
saveToFileAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateHealthChecks() {
|
||||
mShortTaskHandler.post(() -> {
|
||||
try {
|
||||
Slog.i(TAG, "Updating explicit health checks for all available packages");
|
||||
mHealthCheckController.getSupportedPackages(supported -> {
|
||||
try {
|
||||
mHealthCheckController.getRequestedPackages(
|
||||
requested -> updateChecksInner(supported, requested));
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "Failed to get requested health check packages", e);
|
||||
}
|
||||
});
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "Failed to get supported health check package", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters {@code observer} from listening to package failure.
|
||||
* Additionally, this stops observing any packages that may have previously been observed
|
||||
@@ -250,7 +382,7 @@ public class PackageWatchdog {
|
||||
* <p>This method could be called frequently if there is a severe problem on the device.
|
||||
*/
|
||||
public void onPackageFailure(List<VersionedPackage> packages) {
|
||||
mWorkerHandler.post(() -> {
|
||||
mLongTaskHandler.post(() -> {
|
||||
synchronized (mLock) {
|
||||
if (mAllObservers.isEmpty()) {
|
||||
return;
|
||||
@@ -286,49 +418,32 @@ public class PackageWatchdog {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the observers monitoring {@code packageName} that explicit health check has passed.
|
||||
*
|
||||
* <p> This update is strictly for registered observers at the time of the call
|
||||
* Observers that register after this signal will have no knowledge of prior signals and will
|
||||
* effectively behave as if the explicit health check hasn't passed for {@code packageName}.
|
||||
*
|
||||
* <p> {@code packageName} can still be considered failed if reported by
|
||||
* {@link #onPackageFailure} before the package expires.
|
||||
*
|
||||
* <p> Triggered by components outside the system server when they are fully functional after an
|
||||
* update.
|
||||
*/
|
||||
public void onExplicitHealthCheckPassed(String packageName) {
|
||||
Slog.i(TAG, "Health check passed for package: " + packageName);
|
||||
boolean shouldUpdateFile = false;
|
||||
synchronized (mLock) {
|
||||
for (int observerIdx = 0; observerIdx < mAllObservers.size(); observerIdx++) {
|
||||
ObserverInternal observer = mAllObservers.valueAt(observerIdx);
|
||||
MonitoredPackage monitoredPackage = observer.mPackages.get(packageName);
|
||||
if (monitoredPackage != null && !monitoredPackage.mHasPassedHealthCheck) {
|
||||
monitoredPackage.mHasPassedHealthCheck = true;
|
||||
shouldUpdateFile = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldUpdateFile) {
|
||||
saveToFileAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(b/120598832): Optimize write? Maybe only write a separate smaller file?
|
||||
// This currently adds about 7ms extra to shutdown thread
|
||||
/** Writes the package information to file during shutdown. */
|
||||
public void writeNow() {
|
||||
if (!mAllObservers.isEmpty()) {
|
||||
mWorkerHandler.removeCallbacks(this::saveToFile);
|
||||
mLongTaskHandler.removeCallbacks(this::saveToFile);
|
||||
pruneObservers(SystemClock.uptimeMillis() - mUptimeAtLastRescheduleMs);
|
||||
saveToFile();
|
||||
Slog.i(TAG, "Last write to update package durations");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(b/120598832): Set depending on DeviceConfig flag
|
||||
/**
|
||||
* Enables or disables explicit health checks.
|
||||
* <p> If explicit health checks are enabled, the health check service is started.
|
||||
* <p> If explicit health checks are disabled, pending explicit health check requests are
|
||||
* passed and the health check service is stopped.
|
||||
*/
|
||||
public void setExplicitHealthCheckEnabled(boolean enabled) {
|
||||
synchronized (mLock) {
|
||||
mIsHealthCheckEnabled = enabled;
|
||||
mHealthCheckController.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
/** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. */
|
||||
@Retention(SOURCE)
|
||||
@IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_NONE,
|
||||
@@ -371,6 +486,37 @@ public class PackageWatchdog {
|
||||
String getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the observers monitoring {@code packageName} that explicit health check has passed.
|
||||
*
|
||||
* <p> This update is strictly for registered observers at the time of the call
|
||||
* Observers that register after this signal will have no knowledge of prior signals and will
|
||||
* effectively behave as if the explicit health check hasn't passed for {@code packageName}.
|
||||
*
|
||||
* <p> {@code packageName} can still be considered failed if reported by
|
||||
* {@link #onPackageFailure} before the package expires.
|
||||
*
|
||||
* <p> Triggered by components outside the system server when they are fully functional after an
|
||||
* update.
|
||||
*/
|
||||
private void onHealthCheckPassed(String packageName) {
|
||||
Slog.i(TAG, "Health check passed for package: " + packageName);
|
||||
boolean shouldUpdateFile = false;
|
||||
synchronized (mLock) {
|
||||
for (int observerIdx = 0; observerIdx < mAllObservers.size(); observerIdx++) {
|
||||
ObserverInternal observer = mAllObservers.valueAt(observerIdx);
|
||||
MonitoredPackage monitoredPackage = observer.mPackages.get(packageName);
|
||||
if (monitoredPackage != null && !monitoredPackage.mHasPassedHealthCheck) {
|
||||
monitoredPackage.mHasPassedHealthCheck = true;
|
||||
shouldUpdateFile = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldUpdateFile) {
|
||||
saveToFileAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/** Reschedules handler to prune expired packages from observers. */
|
||||
private void rescheduleCleanup() {
|
||||
synchronized (mLock) {
|
||||
@@ -393,8 +539,8 @@ public class PackageWatchdog {
|
||||
|| nextDurationToScheduleMs < remainingDurationMs) {
|
||||
// First schedule or an earlier reschedule
|
||||
pruneObservers(elapsedDurationMs);
|
||||
mTimerHandler.removeCallbacks(mPackageCleanup);
|
||||
mTimerHandler.postDelayed(mPackageCleanup, nextDurationToScheduleMs);
|
||||
mShortTaskHandler.removeCallbacks(mPackageCleanup);
|
||||
mShortTaskHandler.postDelayed(mPackageCleanup, nextDurationToScheduleMs);
|
||||
mDurationAtLastReschedule = nextDurationToScheduleMs;
|
||||
mUptimeAtLastRescheduleMs = uptimeMs;
|
||||
}
|
||||
@@ -437,7 +583,7 @@ public class PackageWatchdog {
|
||||
List<MonitoredPackage> failedPackages =
|
||||
observer.updateMonitoringDurations(elapsedMs);
|
||||
if (!failedPackages.isEmpty()) {
|
||||
onExplicitHealthCheckFailed(observer, failedPackages);
|
||||
onHealthCheckFailed(observer, failedPackages);
|
||||
}
|
||||
if (observer.mPackages.isEmpty()) {
|
||||
Slog.i(TAG, "Discarding observer " + observer.mName + ". All packages expired");
|
||||
@@ -445,12 +591,13 @@ public class PackageWatchdog {
|
||||
}
|
||||
}
|
||||
}
|
||||
updateHealthChecks();
|
||||
saveToFileAsync();
|
||||
}
|
||||
|
||||
private void onExplicitHealthCheckFailed(ObserverInternal observer,
|
||||
private void onHealthCheckFailed(ObserverInternal observer,
|
||||
List<MonitoredPackage> failedPackages) {
|
||||
mWorkerHandler.post(() -> {
|
||||
mLongTaskHandler.post(() -> {
|
||||
synchronized (mLock) {
|
||||
PackageHealthObserver registeredObserver = observer.mRegisteredObserver;
|
||||
if (registeredObserver != null) {
|
||||
@@ -458,6 +605,7 @@ public class PackageWatchdog {
|
||||
for (int i = 0; i < failedPackages.size(); i++) {
|
||||
String packageName = failedPackages.get(i).mName;
|
||||
long versionCode = 0;
|
||||
Slog.i(TAG, "Explicit health check failed for package " + packageName);
|
||||
try {
|
||||
versionCode = pm.getPackageInfo(
|
||||
packageName, 0 /* flags */).getLongVersionCode();
|
||||
@@ -498,7 +646,7 @@ public class PackageWatchdog {
|
||||
} catch (FileNotFoundException e) {
|
||||
// Nothing to monitor
|
||||
} catch (IOException | NumberFormatException | XmlPullParserException e) {
|
||||
Log.wtf(TAG, "Unable to read monitored packages, deleting file", e);
|
||||
Slog.wtf(TAG, "Unable to read monitored packages, deleting file", e);
|
||||
mPolicyFile.delete();
|
||||
} finally {
|
||||
IoUtils.closeQuietly(infile);
|
||||
@@ -542,8 +690,8 @@ public class PackageWatchdog {
|
||||
}
|
||||
|
||||
private void saveToFileAsync() {
|
||||
mWorkerHandler.removeCallbacks(this::saveToFile);
|
||||
mWorkerHandler.post(this::saveToFile);
|
||||
mLongTaskHandler.removeCallbacks(this::saveToFile);
|
||||
mLongTaskHandler.post(this::saveToFile);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -166,8 +166,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve
|
||||
* This may cause {@code packages} to be rolled back if they crash too freqeuntly.
|
||||
*/
|
||||
public void startObservingHealth(List<String> packages, long durationMs) {
|
||||
PackageWatchdog.getInstance(mContext).startObservingHealth(this, packages, durationMs,
|
||||
false /* withExplicitHealthCheck */);
|
||||
PackageWatchdog.getInstance(mContext).startObservingHealth(this, packages, durationMs);
|
||||
}
|
||||
|
||||
/** Verifies the rollback state after a reboot. */
|
||||
|
||||
@@ -19,11 +19,16 @@ package com.android.server;
|
||||
import static com.android.server.PackageWatchdog.TRIGGER_FAILURE_COUNT;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.VersionedPackage;
|
||||
import android.os.Handler;
|
||||
import android.os.RemoteException;
|
||||
import android.os.test.TestLooper;
|
||||
import android.util.AtomicFile;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
|
||||
@@ -36,11 +41,14 @@ import org.junit.Test;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
// TODO(zezeozue): Write test without using PackageWatchdog#getPackages. Just rely on
|
||||
// TODO: Write test without using PackageWatchdog#getPackages. Just rely on
|
||||
// behavior of observers receiving crash notifications or not to determine if it's registered
|
||||
// TODO: Use Truth in tests.
|
||||
/**
|
||||
* Test PackageWatchdog.
|
||||
*/
|
||||
@@ -77,12 +85,11 @@ public class PackageWatchdogTest {
|
||||
TestObserver observer3 = new TestObserver(OBSERVER_NAME_3);
|
||||
|
||||
// Start observing for observer1 which will be unregistered
|
||||
watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION, false);
|
||||
watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION);
|
||||
// Start observing for observer2 which will expire
|
||||
watchdog.startObservingHealth(observer2, Arrays.asList(APP_A, APP_B), SHORT_DURATION,
|
||||
false);
|
||||
watchdog.startObservingHealth(observer2, Arrays.asList(APP_A, APP_B), SHORT_DURATION);
|
||||
// Start observing for observer3 which will have expiry duration reduced
|
||||
watchdog.startObservingHealth(observer3, Arrays.asList(APP_A), LONG_DURATION, false);
|
||||
watchdog.startObservingHealth(observer3, Arrays.asList(APP_A), LONG_DURATION);
|
||||
|
||||
// Verify packages observed at start
|
||||
// 1
|
||||
@@ -145,9 +152,8 @@ public class PackageWatchdogTest {
|
||||
TestObserver observer1 = new TestObserver(OBSERVER_NAME_1);
|
||||
TestObserver observer2 = new TestObserver(OBSERVER_NAME_2);
|
||||
|
||||
watchdog1.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION, false);
|
||||
watchdog1.startObservingHealth(observer2, Arrays.asList(APP_A, APP_B), SHORT_DURATION,
|
||||
false);
|
||||
watchdog1.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION);
|
||||
watchdog1.startObservingHealth(observer2, Arrays.asList(APP_A, APP_B), SHORT_DURATION);
|
||||
|
||||
// Verify 2 observers are registered and saved internally
|
||||
// 1
|
||||
@@ -193,8 +199,8 @@ public class PackageWatchdogTest {
|
||||
TestObserver observer1 = new TestObserver(OBSERVER_NAME_1);
|
||||
TestObserver observer2 = new TestObserver(OBSERVER_NAME_2);
|
||||
|
||||
watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION, false);
|
||||
watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION, false);
|
||||
watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION);
|
||||
watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION);
|
||||
|
||||
// Then fail APP_A below the threshold
|
||||
for (int i = 0; i < TRIGGER_FAILURE_COUNT - 1; i++) {
|
||||
@@ -220,8 +226,8 @@ public class PackageWatchdogTest {
|
||||
TestObserver observer2 = new TestObserver(OBSERVER_NAME_2);
|
||||
|
||||
|
||||
watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION, false);
|
||||
watchdog.startObservingHealth(observer1, Arrays.asList(APP_B), SHORT_DURATION, false);
|
||||
watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION);
|
||||
watchdog.startObservingHealth(observer1, Arrays.asList(APP_B), SHORT_DURATION);
|
||||
|
||||
// Then fail APP_C (not observed) above the threshold
|
||||
for (int i = 0; i < TRIGGER_FAILURE_COUNT; i++) {
|
||||
@@ -255,7 +261,7 @@ public class PackageWatchdogTest {
|
||||
}
|
||||
};
|
||||
|
||||
watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION, false);
|
||||
watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION);
|
||||
|
||||
// Then fail APP_A (different version) above the threshold
|
||||
for (int i = 0; i < TRIGGER_FAILURE_COUNT; i++) {
|
||||
@@ -288,13 +294,13 @@ public class PackageWatchdogTest {
|
||||
|
||||
// Start observing for all impact observers
|
||||
watchdog.startObservingHealth(observerNone, Arrays.asList(APP_A, APP_B, APP_C, APP_D),
|
||||
SHORT_DURATION, false);
|
||||
SHORT_DURATION);
|
||||
watchdog.startObservingHealth(observerHigh, Arrays.asList(APP_A, APP_B, APP_C),
|
||||
SHORT_DURATION, false);
|
||||
SHORT_DURATION);
|
||||
watchdog.startObservingHealth(observerMid, Arrays.asList(APP_A, APP_B),
|
||||
SHORT_DURATION, false);
|
||||
SHORT_DURATION);
|
||||
watchdog.startObservingHealth(observerLow, Arrays.asList(APP_A),
|
||||
SHORT_DURATION, false);
|
||||
SHORT_DURATION);
|
||||
|
||||
// Then fail all apps above the threshold
|
||||
for (int i = 0; i < TRIGGER_FAILURE_COUNT; i++) {
|
||||
@@ -346,8 +352,8 @@ public class PackageWatchdogTest {
|
||||
PackageHealthObserverImpact.USER_IMPACT_MEDIUM);
|
||||
|
||||
// Start observing for observerFirst and observerSecond with failure handling
|
||||
watchdog.startObservingHealth(observerFirst, Arrays.asList(APP_A), LONG_DURATION, false);
|
||||
watchdog.startObservingHealth(observerSecond, Arrays.asList(APP_A), LONG_DURATION, false);
|
||||
watchdog.startObservingHealth(observerFirst, Arrays.asList(APP_A), LONG_DURATION);
|
||||
watchdog.startObservingHealth(observerSecond, Arrays.asList(APP_A), LONG_DURATION);
|
||||
|
||||
// Then fail APP_A above the threshold
|
||||
for (int i = 0; i < TRIGGER_FAILURE_COUNT; i++) {
|
||||
@@ -424,8 +430,8 @@ public class PackageWatchdogTest {
|
||||
PackageHealthObserverImpact.USER_IMPACT_HIGH);
|
||||
|
||||
// Start observing for observer1 and observer2 with failure handling
|
||||
watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION, false);
|
||||
watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION, false);
|
||||
watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION);
|
||||
watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION);
|
||||
|
||||
// Then fail APP_A above the threshold
|
||||
for (int i = 0; i < TRIGGER_FAILURE_COUNT; i++) {
|
||||
@@ -442,11 +448,12 @@ public class PackageWatchdogTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test explicit health check status determines package failure or success on expiry
|
||||
* Test package passing explicit health checks does not fail and vice versa.
|
||||
*/
|
||||
@Test
|
||||
public void testPackageFailureExplicitHealthCheck() throws Exception {
|
||||
PackageWatchdog watchdog = createWatchdog();
|
||||
public void testExplicitHealthChecks() throws Exception {
|
||||
TestController controller = new TestController();
|
||||
PackageWatchdog watchdog = createWatchdog(controller, true /* withPackagesReady */);
|
||||
TestObserver observer1 = new TestObserver(OBSERVER_NAME_1,
|
||||
PackageHealthObserverImpact.USER_IMPACT_HIGH);
|
||||
TestObserver observer2 = new TestObserver(OBSERVER_NAME_2,
|
||||
@@ -457,21 +464,36 @@ public class PackageWatchdogTest {
|
||||
|
||||
// Start observing with explicit health checks for APP_A and APP_B respectively
|
||||
// with observer1 and observer2
|
||||
watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION, true);
|
||||
watchdog.startObservingHealth(observer2, Arrays.asList(APP_B), SHORT_DURATION, true);
|
||||
// Explicit health check passed for APP_A (observer1 is aware)
|
||||
watchdog.onExplicitHealthCheckPassed(APP_A);
|
||||
// Start observing APP_A with explicit health checks for observer3.
|
||||
controller.setSupportedPackages(Arrays.asList(APP_A, APP_B));
|
||||
watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION);
|
||||
watchdog.startObservingHealth(observer2, Arrays.asList(APP_B), SHORT_DURATION);
|
||||
|
||||
// Run handler so requests are dispatched to the controller
|
||||
mTestLooper.dispatchAll();
|
||||
|
||||
// Verify we requested health checks for APP_A and APP_B
|
||||
List<String> requestedPackages = controller.getRequestedPackages();
|
||||
assertEquals(2, requestedPackages.size());
|
||||
assertEquals(APP_A, requestedPackages.get(0));
|
||||
assertEquals(APP_B, requestedPackages.get(1));
|
||||
|
||||
// Then health check passed for APP_A (observer1 is aware)
|
||||
controller.setPackagePassed(APP_A);
|
||||
|
||||
// Then start observing APP_A with explicit health checks for observer3.
|
||||
// Observer3 didn't exist when we got the explicit health check above, so
|
||||
// it starts out with a non-passing explicit health check and has to wait for a pass
|
||||
// otherwise it would be notified of APP_A failure on expiry
|
||||
watchdog.startObservingHealth(observer3, Arrays.asList(APP_A), SHORT_DURATION, true);
|
||||
watchdog.startObservingHealth(observer3, Arrays.asList(APP_A), SHORT_DURATION);
|
||||
|
||||
// Then expire observers
|
||||
Thread.sleep(SHORT_DURATION);
|
||||
// Run handler so package failures are dispatched to observers
|
||||
mTestLooper.dispatchAll();
|
||||
|
||||
// Verify we cancelled all requests on expiry
|
||||
assertEquals(0, controller.getRequestedPackages().size());
|
||||
|
||||
// Verify observer1 is not notified
|
||||
assertEquals(0, observer1.mFailedPackages.size());
|
||||
|
||||
@@ -484,9 +506,96 @@ public class PackageWatchdogTest {
|
||||
assertEquals(APP_A, observer3.mFailedPackages.get(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test explicit health check state can be disabled and enabled correctly.
|
||||
*/
|
||||
@Test
|
||||
public void testExplicitHealthCheckStateChanges() throws Exception {
|
||||
TestController controller = new TestController();
|
||||
PackageWatchdog watchdog = createWatchdog(controller, true /* withPackagesReady */);
|
||||
TestObserver observer = new TestObserver(OBSERVER_NAME_1,
|
||||
PackageHealthObserverImpact.USER_IMPACT_MEDIUM);
|
||||
|
||||
// Start observing with explicit health checks for APP_A and APP_B
|
||||
controller.setSupportedPackages(Arrays.asList(APP_A, APP_B, APP_C));
|
||||
watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION);
|
||||
watchdog.startObservingHealth(observer, Arrays.asList(APP_B), LONG_DURATION);
|
||||
|
||||
// Run handler so requests are dispatched to the controller
|
||||
mTestLooper.dispatchAll();
|
||||
|
||||
// Verify we requested health checks for APP_A and APP_B
|
||||
List<String> requestedPackages = controller.getRequestedPackages();
|
||||
assertEquals(2, requestedPackages.size());
|
||||
assertEquals(APP_A, requestedPackages.get(0));
|
||||
assertEquals(APP_B, requestedPackages.get(1));
|
||||
|
||||
// Disable explicit health checks (marks APP_A and APP_B as passed)
|
||||
watchdog.setExplicitHealthCheckEnabled(false);
|
||||
|
||||
// Run handler so requests/cancellations are dispatched to the controller
|
||||
mTestLooper.dispatchAll();
|
||||
|
||||
// Verify all checks are cancelled
|
||||
assertEquals(0, controller.getRequestedPackages().size());
|
||||
|
||||
// Then expire APP_A
|
||||
Thread.sleep(SHORT_DURATION);
|
||||
mTestLooper.dispatchAll();
|
||||
|
||||
// Verify APP_A is not failed (APP_B) is not expired yet
|
||||
assertEquals(0, observer.mFailedPackages.size());
|
||||
|
||||
// Re-enable explicit health checks
|
||||
watchdog.setExplicitHealthCheckEnabled(true);
|
||||
|
||||
// Run handler so requests/cancellations are dispatched to the controller
|
||||
mTestLooper.dispatchAll();
|
||||
|
||||
// Verify no requests are made cos APP_A is expired and APP_B was marked as passed
|
||||
assertEquals(0, controller.getRequestedPackages().size());
|
||||
|
||||
// Then set new supported packages
|
||||
controller.setSupportedPackages(Arrays.asList(APP_C));
|
||||
// Start observing APP_A and APP_C; only APP_C has support for explicit health checks
|
||||
watchdog.startObservingHealth(observer, Arrays.asList(APP_A, APP_C), SHORT_DURATION);
|
||||
|
||||
// Run handler so requests/cancellations are dispatched to the controller
|
||||
mTestLooper.dispatchAll();
|
||||
|
||||
// Verify requests are only made for APP_C
|
||||
requestedPackages = controller.getRequestedPackages();
|
||||
assertEquals(1, requestedPackages.size());
|
||||
assertEquals(APP_C, requestedPackages.get(0));
|
||||
|
||||
// Then expire APP_A and APP_C
|
||||
Thread.sleep(SHORT_DURATION);
|
||||
mTestLooper.dispatchAll();
|
||||
|
||||
// Verify only APP_C is failed because explicit health checks was not supported for APP_A
|
||||
assertEquals(1, observer.mFailedPackages.size());
|
||||
assertEquals(APP_C, observer.mFailedPackages.get(0));
|
||||
}
|
||||
|
||||
private PackageWatchdog createWatchdog() {
|
||||
return new PackageWatchdog(InstrumentationRegistry.getContext(),
|
||||
mTestLooper.getLooper());
|
||||
return createWatchdog(new TestController(), true /* withPackagesReady */);
|
||||
}
|
||||
|
||||
private PackageWatchdog createWatchdog(TestController controller, boolean withPackagesReady) {
|
||||
Context context = InstrumentationRegistry.getContext();
|
||||
AtomicFile policyFile =
|
||||
new AtomicFile(new File(context.getFilesDir(), "package-watchdog.xml"));
|
||||
Handler handler = new Handler(mTestLooper.getLooper());
|
||||
PackageWatchdog watchdog =
|
||||
new PackageWatchdog(context, policyFile, handler, handler, controller);
|
||||
// Verify controller is not automatically started
|
||||
assertFalse(controller.mIsEnabled);
|
||||
if (withPackagesReady) {
|
||||
watchdog.onPackagesReady();
|
||||
// Verify controller by default is started when packages are ready
|
||||
assertTrue(controller.mIsEnabled);
|
||||
}
|
||||
return watchdog;
|
||||
}
|
||||
|
||||
private static class TestObserver implements PackageHealthObserver {
|
||||
@@ -517,4 +626,69 @@ public class PackageWatchdogTest {
|
||||
return mName;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestController extends ExplicitHealthCheckController {
|
||||
TestController() {
|
||||
super(null /* controller */);
|
||||
}
|
||||
|
||||
private boolean mIsEnabled;
|
||||
private List<String> mSupportedPackages = new ArrayList<>();
|
||||
private List<String> mRequestedPackages = new ArrayList<>();
|
||||
private Runnable mStateChangedRunnable;
|
||||
private Consumer<String> mPassedConsumer;
|
||||
|
||||
@Override
|
||||
public void request(String packageName) throws RemoteException {
|
||||
if (!mRequestedPackages.contains(packageName)) {
|
||||
mRequestedPackages.add(packageName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel(String packageName) throws RemoteException {
|
||||
mRequestedPackages.remove(packageName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getSupportedPackages(Consumer<List<String>> consumer) throws RemoteException {
|
||||
consumer.accept(mIsEnabled ? mSupportedPackages : Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getRequestedPackages(Consumer<List<String>> consumer) throws RemoteException {
|
||||
// Pass copy to prevent ConcurrentModificationException during test
|
||||
consumer.accept(
|
||||
mIsEnabled ? new ArrayList<>(mRequestedPackages) : Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
mIsEnabled = enabled;
|
||||
mStateChangedRunnable.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCallbacks(Runnable stateChangedRunnable, Consumer<String> passedConsumer) {
|
||||
mStateChangedRunnable = stateChangedRunnable;
|
||||
mPassedConsumer = passedConsumer;
|
||||
}
|
||||
|
||||
public void setSupportedPackages(List<String> packages) {
|
||||
mSupportedPackages.clear();
|
||||
mSupportedPackages.addAll(packages);
|
||||
}
|
||||
|
||||
public void setPackagePassed(String packageName) {
|
||||
mPassedConsumer.accept(packageName);
|
||||
}
|
||||
|
||||
public List<String> getRequestedPackages() {
|
||||
if (mIsEnabled) {
|
||||
return mRequestedPackages;
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user