1. Receiving List<PackageInfo>:
Since I29e2d619a5296716c29893ab3aa2f35f69bfb4d7, we now receive a
List of PackageInfo instead of Strings for packages supporting
explicit health checks. Now, we parse this List<PackageInfo> from
ExtServices instead of trying to parse List<String> and we use the
health check timeout in the PackageInfo as the health check expiry
deadline instead of using the total package expiry time.
2. Updating health check durations onSupportedPackages:
Before, we always updated the health check duration for a
package if the package is supported and the health check state is
not PASSED, this caused the health check duration for a package to
never reduce as long as we kept getting onSupportedPackages. Now, we
improved the readability of the state transitions onSupportedPackages.
We now correctly only update the health check duration for supported
packages in the INACTIVE state.
3. FAILED state:
Before we only had INACTIVE, ACTIVE and PASSED states. When a package
has failed the health check we could notify the observer multiple
times in quick succession and get into a bad internal state with
negative health check durations. Now we added check to ensure we
don't try to schedule with a Handler with a negative duration and we
defined a negative health check duration to be a new FAILED state if the
health check is not passed. This clearly defines the state transitions
as seen below:
+----------+ +---------+ +------+
| | | | | |
| INACTIVE +---->+ ACTIVE +--->+PASSED|
| | | | | |
+-----+----+ +----+----+ +------+
| |
| |
| |
| |
| +----v----+
| | |
+----------> FAILED |
| |
+---------+
4. Uptime state:
Everytime we pruned observers, we scheduled the next prune and stored
the current SystemClock#uptimeMillis. This allowed us determine how
much time had elapsed for the next prune. The uptime was not correclty
updated when starting to observe already observed packages. With the
following sequence of events:
-monitor package A for 1hr
-30mins elapsed
-monitor package A again for 1hr
A would expire 30mins from the last event instead of 1hr.
This was because the second time around, we
saved the new state to disk but did not reschedule so did not update
the uptime at last schedule, so 1hr from the first event, we would
prune packages with the original uptime and incorrectly expire A
earlier. Now we update all internal state, fixed this and added a test
for this case.
5. Readability
Improved method variable names, logging and comments.
Bug: 120598832
Test: Manual testing && atest PackageWatcdogTest
Change-Id: I1512d5938848ad26b668636405fe9b0db50d3a2e
797 lines
34 KiB
Java
797 lines
34 KiB
Java
/*
|
|
* Copyright (C) 2019 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;
|
|
|
|
import static com.android.server.PackageWatchdog.MonitoredPackage;
|
|
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.test.TestLooper;
|
|
import android.service.watchdog.PackageInfo;
|
|
import android.util.AtomicFile;
|
|
|
|
import androidx.test.InstrumentationRegistry;
|
|
|
|
import com.android.server.PackageWatchdog.PackageHealthObserver;
|
|
import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
|
|
|
|
import org.junit.Before;
|
|
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.Set;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.function.Consumer;
|
|
|
|
// 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.
|
|
*/
|
|
public class PackageWatchdogTest {
|
|
private static final String APP_A = "com.package.a";
|
|
private static final String APP_B = "com.package.b";
|
|
private static final String APP_C = "com.package.c";
|
|
private static final String APP_D = "com.package.d";
|
|
private static final long VERSION_CODE = 1L;
|
|
private static final String OBSERVER_NAME_1 = "observer1";
|
|
private static final String OBSERVER_NAME_2 = "observer2";
|
|
private static final String OBSERVER_NAME_3 = "observer3";
|
|
private static final String OBSERVER_NAME_4 = "observer4";
|
|
private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1);
|
|
private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(5);
|
|
private TestLooper mTestLooper;
|
|
|
|
@Before
|
|
public void setUp() throws Exception {
|
|
new File(InstrumentationRegistry.getContext().getFilesDir(),
|
|
"package-watchdog.xml").delete();
|
|
mTestLooper = new TestLooper();
|
|
mTestLooper.startAutoDispatch();
|
|
}
|
|
|
|
/**
|
|
* Test registration, unregistration, package expiry and duration reduction
|
|
*/
|
|
@Test
|
|
public void testRegistration() throws Exception {
|
|
PackageWatchdog watchdog = createWatchdog();
|
|
TestObserver observer1 = new TestObserver(OBSERVER_NAME_1);
|
|
TestObserver observer2 = new TestObserver(OBSERVER_NAME_2);
|
|
TestObserver observer3 = new TestObserver(OBSERVER_NAME_3);
|
|
|
|
// Start observing for observer1 which will be unregistered
|
|
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);
|
|
// Start observing for observer3 which will have expiry duration reduced
|
|
watchdog.startObservingHealth(observer3, Arrays.asList(APP_A), LONG_DURATION);
|
|
|
|
// Verify packages observed at start
|
|
// 1
|
|
assertEquals(1, watchdog.getPackages(observer1).size());
|
|
assertTrue(watchdog.getPackages(observer1).contains(APP_A));
|
|
// 2
|
|
assertEquals(2, watchdog.getPackages(observer2).size());
|
|
assertTrue(watchdog.getPackages(observer2).contains(APP_A));
|
|
assertTrue(watchdog.getPackages(observer2).contains(APP_B));
|
|
// 3
|
|
assertEquals(1, watchdog.getPackages(observer3).size());
|
|
assertTrue(watchdog.getPackages(observer3).contains(APP_A));
|
|
|
|
// Then unregister observer1
|
|
watchdog.unregisterHealthObserver(observer1);
|
|
|
|
// Verify observer2 and observer3 left
|
|
// 1
|
|
assertNull(watchdog.getPackages(observer1));
|
|
// 2
|
|
assertEquals(2, watchdog.getPackages(observer2).size());
|
|
assertTrue(watchdog.getPackages(observer2).contains(APP_A));
|
|
assertTrue(watchdog.getPackages(observer2).contains(APP_B));
|
|
// 3
|
|
assertEquals(1, watchdog.getPackages(observer3).size());
|
|
assertTrue(watchdog.getPackages(observer3).contains(APP_A));
|
|
|
|
// Then advance time a little and run messages in Handlers so observer2 expires
|
|
Thread.sleep(SHORT_DURATION);
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Verify observer3 left with reduced expiry duration
|
|
// 1
|
|
assertNull(watchdog.getPackages(observer1));
|
|
// 2
|
|
assertNull(watchdog.getPackages(observer2));
|
|
// 3
|
|
assertEquals(1, watchdog.getPackages(observer3).size());
|
|
assertTrue(watchdog.getPackages(observer3).contains(APP_A));
|
|
|
|
// Then advance time some more and run messages in Handlers so observer3 expires
|
|
Thread.sleep(LONG_DURATION);
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Verify observer3 expired
|
|
// 1
|
|
assertNull(watchdog.getPackages(observer1));
|
|
// 2
|
|
assertNull(watchdog.getPackages(observer2));
|
|
// 3
|
|
assertNull(watchdog.getPackages(observer3));
|
|
}
|
|
|
|
/** Observing already observed package extends the observation time. */
|
|
@Test
|
|
public void testObserveAlreadyObservedPackage() throws Exception {
|
|
PackageWatchdog watchdog = createWatchdog();
|
|
TestObserver observer = new TestObserver(OBSERVER_NAME_1);
|
|
|
|
// Start observing APP_A
|
|
watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION);
|
|
|
|
// Then advance time half-way
|
|
Thread.sleep(SHORT_DURATION / 2);
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Start observing APP_A again
|
|
watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION);
|
|
|
|
// Then advance time such that it should have expired were it not for the second observation
|
|
Thread.sleep((SHORT_DURATION / 2) + 1);
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Verify that APP_A not expired since second observation extended the time
|
|
assertEquals(1, watchdog.getPackages(observer).size());
|
|
assertTrue(watchdog.getPackages(observer).contains(APP_A));
|
|
}
|
|
|
|
/**
|
|
* Test package observers are persisted and loaded on startup
|
|
*/
|
|
@Test
|
|
public void testPersistence() throws Exception {
|
|
PackageWatchdog watchdog1 = createWatchdog();
|
|
TestObserver observer1 = new TestObserver(OBSERVER_NAME_1);
|
|
TestObserver observer2 = new TestObserver(OBSERVER_NAME_2);
|
|
|
|
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
|
|
assertEquals(1, watchdog1.getPackages(observer1).size());
|
|
assertTrue(watchdog1.getPackages(observer1).contains(APP_A));
|
|
// 2
|
|
assertEquals(2, watchdog1.getPackages(observer2).size());
|
|
assertTrue(watchdog1.getPackages(observer2).contains(APP_A));
|
|
assertTrue(watchdog1.getPackages(observer2).contains(APP_B));
|
|
|
|
// Then advance time and run IO Handler so file is saved
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Then start a new watchdog
|
|
PackageWatchdog watchdog2 = createWatchdog();
|
|
|
|
// Verify the new watchdog loads observers on startup but nothing registered
|
|
assertEquals(0, watchdog2.getPackages(observer1).size());
|
|
assertEquals(0, watchdog2.getPackages(observer2).size());
|
|
// Verify random observer not saved returns null
|
|
assertNull(watchdog2.getPackages(new TestObserver(OBSERVER_NAME_3)));
|
|
|
|
// Then regiser observer1
|
|
watchdog2.registerHealthObserver(observer1);
|
|
watchdog2.registerHealthObserver(observer2);
|
|
|
|
// Verify 2 observers are registered after reload
|
|
// 1
|
|
assertEquals(1, watchdog1.getPackages(observer1).size());
|
|
assertTrue(watchdog1.getPackages(observer1).contains(APP_A));
|
|
// 2
|
|
assertEquals(2, watchdog1.getPackages(observer2).size());
|
|
assertTrue(watchdog1.getPackages(observer2).contains(APP_A));
|
|
assertTrue(watchdog1.getPackages(observer2).contains(APP_B));
|
|
}
|
|
|
|
/**
|
|
* Test package failure under threshold does not notify observers
|
|
*/
|
|
@Test
|
|
public void testNoPackageFailureBeforeThreshold() throws Exception {
|
|
PackageWatchdog watchdog = createWatchdog();
|
|
TestObserver observer1 = new TestObserver(OBSERVER_NAME_1);
|
|
TestObserver observer2 = new TestObserver(OBSERVER_NAME_2);
|
|
|
|
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++) {
|
|
watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
|
|
}
|
|
|
|
// Run handler so package failures are dispatched to observers
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Verify that observers are not notified
|
|
assertEquals(0, observer1.mFailedPackages.size());
|
|
assertEquals(0, observer2.mFailedPackages.size());
|
|
}
|
|
|
|
/**
|
|
* Test package failure and does not notify any observer because they are not observing
|
|
* the failed packages.
|
|
*/
|
|
@Test
|
|
public void testPackageFailureDifferentPackageNotifyNone() throws Exception {
|
|
PackageWatchdog watchdog = createWatchdog();
|
|
TestObserver observer1 = new TestObserver(OBSERVER_NAME_1);
|
|
TestObserver observer2 = new TestObserver(OBSERVER_NAME_2);
|
|
|
|
|
|
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++) {
|
|
watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_C, VERSION_CODE)));
|
|
}
|
|
|
|
// Run handler so package failures are dispatched to observers
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Verify that observers are not notified
|
|
assertEquals(0, observer1.mFailedPackages.size());
|
|
assertEquals(0, observer2.mFailedPackages.size());
|
|
}
|
|
|
|
/**
|
|
* Test package failure and does not notify any observer because the failed package version
|
|
* does not match the available rollback-from-version.
|
|
*/
|
|
@Test
|
|
public void testPackageFailureDifferentVersionNotifyNone() throws Exception {
|
|
PackageWatchdog watchdog = createWatchdog();
|
|
long differentVersionCode = 2L;
|
|
TestObserver observer = new TestObserver(OBSERVER_NAME_1) {
|
|
@Override
|
|
public int onHealthCheckFailed(VersionedPackage versionedPackage) {
|
|
if (versionedPackage.getVersionCode() == VERSION_CODE) {
|
|
// Only rollback for specific versionCode
|
|
return PackageHealthObserverImpact.USER_IMPACT_MEDIUM;
|
|
}
|
|
return PackageHealthObserverImpact.USER_IMPACT_NONE;
|
|
}
|
|
};
|
|
|
|
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++) {
|
|
watchdog.onPackageFailure(Arrays.asList(
|
|
new VersionedPackage(APP_A, differentVersionCode)));
|
|
}
|
|
|
|
// Run handler so package failures are dispatched to observers
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Verify that observers are not notified
|
|
assertEquals(0, observer.mFailedPackages.size());
|
|
}
|
|
|
|
|
|
/**
|
|
* Test package failure and notifies only least impact observers.
|
|
*/
|
|
@Test
|
|
public void testPackageFailureNotifyAllDifferentImpacts() throws Exception {
|
|
PackageWatchdog watchdog = createWatchdog();
|
|
TestObserver observerNone = new TestObserver(OBSERVER_NAME_1,
|
|
PackageHealthObserverImpact.USER_IMPACT_NONE);
|
|
TestObserver observerHigh = new TestObserver(OBSERVER_NAME_2,
|
|
PackageHealthObserverImpact.USER_IMPACT_HIGH);
|
|
TestObserver observerMid = new TestObserver(OBSERVER_NAME_3,
|
|
PackageHealthObserverImpact.USER_IMPACT_MEDIUM);
|
|
TestObserver observerLow = new TestObserver(OBSERVER_NAME_4,
|
|
PackageHealthObserverImpact.USER_IMPACT_LOW);
|
|
|
|
// Start observing for all impact observers
|
|
watchdog.startObservingHealth(observerNone, Arrays.asList(APP_A, APP_B, APP_C, APP_D),
|
|
SHORT_DURATION);
|
|
watchdog.startObservingHealth(observerHigh, Arrays.asList(APP_A, APP_B, APP_C),
|
|
SHORT_DURATION);
|
|
watchdog.startObservingHealth(observerMid, Arrays.asList(APP_A, APP_B),
|
|
SHORT_DURATION);
|
|
watchdog.startObservingHealth(observerLow, Arrays.asList(APP_A),
|
|
SHORT_DURATION);
|
|
|
|
// Then fail all apps above the threshold
|
|
for (int i = 0; i < TRIGGER_FAILURE_COUNT; i++) {
|
|
watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE),
|
|
new VersionedPackage(APP_B, VERSION_CODE),
|
|
new VersionedPackage(APP_C, VERSION_CODE),
|
|
new VersionedPackage(APP_D, VERSION_CODE)));
|
|
}
|
|
|
|
// Run handler so package failures are dispatched to observers
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Verify least impact observers are notifed of package failures
|
|
List<String> observerNonePackages = observerNone.mFailedPackages;
|
|
List<String> observerHighPackages = observerHigh.mFailedPackages;
|
|
List<String> observerMidPackages = observerMid.mFailedPackages;
|
|
List<String> observerLowPackages = observerLow.mFailedPackages;
|
|
|
|
// APP_D failure observed by only observerNone is not caught cos its impact is none
|
|
assertEquals(0, observerNonePackages.size());
|
|
// APP_C failure is caught by observerHigh cos it's the lowest impact observer
|
|
assertEquals(1, observerHighPackages.size());
|
|
assertEquals(APP_C, observerHighPackages.get(0));
|
|
// APP_B failure is caught by observerMid cos it's the lowest impact observer
|
|
assertEquals(1, observerMidPackages.size());
|
|
assertEquals(APP_B, observerMidPackages.get(0));
|
|
// APP_A failure is caught by observerLow cos it's the lowest impact observer
|
|
assertEquals(1, observerLowPackages.size());
|
|
assertEquals(APP_A, observerLowPackages.get(0));
|
|
}
|
|
|
|
/**
|
|
* Test package failure and least impact observers are notified successively.
|
|
* State transistions:
|
|
*
|
|
* <ul>
|
|
* <li>(observer1:low, observer2:mid) -> {observer1}
|
|
* <li>(observer1:high, observer2:mid) -> {observer2}
|
|
* <li>(observer1:high, observer2:none) -> {observer1}
|
|
* <li>(observer1:none, observer2:none) -> {}
|
|
* <ul>
|
|
*/
|
|
@Test
|
|
public void testPackageFailureNotifyLeastImpactSuccessively() throws Exception {
|
|
PackageWatchdog watchdog = createWatchdog();
|
|
TestObserver observerFirst = new TestObserver(OBSERVER_NAME_1,
|
|
PackageHealthObserverImpact.USER_IMPACT_LOW);
|
|
TestObserver observerSecond = new TestObserver(OBSERVER_NAME_2,
|
|
PackageHealthObserverImpact.USER_IMPACT_MEDIUM);
|
|
|
|
// Start observing for observerFirst and observerSecond with failure handling
|
|
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++) {
|
|
watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
|
|
}
|
|
// Run handler so package failures are dispatched to observers
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Verify only observerFirst is notifed
|
|
assertEquals(1, observerFirst.mFailedPackages.size());
|
|
assertEquals(APP_A, observerFirst.mFailedPackages.get(0));
|
|
assertEquals(0, observerSecond.mFailedPackages.size());
|
|
|
|
// After observerFirst handles failure, next action it has is high impact
|
|
observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_HIGH;
|
|
observerFirst.mFailedPackages.clear();
|
|
observerSecond.mFailedPackages.clear();
|
|
|
|
// Then fail APP_A again above the threshold
|
|
for (int i = 0; i < TRIGGER_FAILURE_COUNT; i++) {
|
|
watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
|
|
}
|
|
// Run handler so package failures are dispatched to observers
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Verify only observerSecond is notifed cos it has least impact
|
|
assertEquals(1, observerSecond.mFailedPackages.size());
|
|
assertEquals(APP_A, observerSecond.mFailedPackages.get(0));
|
|
assertEquals(0, observerFirst.mFailedPackages.size());
|
|
|
|
// After observerSecond handles failure, it has no further actions
|
|
observerSecond.mImpact = PackageHealthObserverImpact.USER_IMPACT_NONE;
|
|
observerFirst.mFailedPackages.clear();
|
|
observerSecond.mFailedPackages.clear();
|
|
|
|
// Then fail APP_A again above the threshold
|
|
for (int i = 0; i < TRIGGER_FAILURE_COUNT; i++) {
|
|
watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
|
|
}
|
|
// Run handler so package failures are dispatched to observers
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Verify only observerFirst is notifed cos it has the only action
|
|
assertEquals(1, observerFirst.mFailedPackages.size());
|
|
assertEquals(APP_A, observerFirst.mFailedPackages.get(0));
|
|
assertEquals(0, observerSecond.mFailedPackages.size());
|
|
|
|
// After observerFirst handles failure, it too has no further actions
|
|
observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_NONE;
|
|
observerFirst.mFailedPackages.clear();
|
|
observerSecond.mFailedPackages.clear();
|
|
|
|
// Then fail APP_A again above the threshold
|
|
for (int i = 0; i < TRIGGER_FAILURE_COUNT; i++) {
|
|
watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
|
|
}
|
|
// Run handler so package failures are dispatched to observers
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Verify no observer is notified cos no actions left
|
|
assertEquals(0, observerFirst.mFailedPackages.size());
|
|
assertEquals(0, observerSecond.mFailedPackages.size());
|
|
}
|
|
|
|
/**
|
|
* Test package failure and notifies only one observer even with observer impact tie.
|
|
*/
|
|
@Test
|
|
public void testPackageFailureNotifyOneSameImpact() throws Exception {
|
|
PackageWatchdog watchdog = createWatchdog();
|
|
TestObserver observer1 = new TestObserver(OBSERVER_NAME_1,
|
|
PackageHealthObserverImpact.USER_IMPACT_HIGH);
|
|
TestObserver observer2 = new TestObserver(OBSERVER_NAME_2,
|
|
PackageHealthObserverImpact.USER_IMPACT_HIGH);
|
|
|
|
// Start observing for observer1 and observer2 with failure handling
|
|
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++) {
|
|
watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)));
|
|
}
|
|
|
|
// Run handler so package failures are dispatched to observers
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Verify only one observer is notifed
|
|
assertEquals(1, observer1.mFailedPackages.size());
|
|
assertEquals(APP_A, observer1.mFailedPackages.get(0));
|
|
assertEquals(0, observer2.mFailedPackages.size());
|
|
}
|
|
|
|
/**
|
|
* Test package passing explicit health checks does not fail and vice versa.
|
|
*/
|
|
@Test
|
|
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,
|
|
PackageHealthObserverImpact.USER_IMPACT_HIGH);
|
|
TestObserver observer3 = new TestObserver(OBSERVER_NAME_3,
|
|
PackageHealthObserverImpact.USER_IMPACT_HIGH);
|
|
|
|
|
|
// Start observing with explicit health checks for APP_A and APP_B respectively
|
|
// with observer1 and observer2
|
|
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);
|
|
|
|
// 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());
|
|
|
|
// Verify observer2 is notifed because health checks for APP_B never passed
|
|
assertEquals(1, observer2.mFailedPackages.size());
|
|
assertEquals(APP_B, observer2.mFailedPackages.get(0));
|
|
|
|
// Verify observer3 is notifed because health checks for APP_A did not pass before expiry
|
|
assertEquals(1, observer3.mFailedPackages.size());
|
|
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));
|
|
}
|
|
|
|
/**
|
|
* Tests failure when health check duration is different from package observation duration
|
|
* Failure is also notified only once.
|
|
*/
|
|
@Test
|
|
public void testExplicitHealthCheckFailureBeforeExpiry() 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
|
|
// package observation duration == LONG_DURATION
|
|
// health check duration == SHORT_DURATION (set by default in the TestController)
|
|
controller.setSupportedPackages(Arrays.asList(APP_A));
|
|
watchdog.startObservingHealth(observer, Arrays.asList(APP_A), LONG_DURATION);
|
|
|
|
// Then APP_A has exceeded health check duration
|
|
Thread.sleep(SHORT_DURATION);
|
|
mTestLooper.dispatchAll();
|
|
|
|
// Verify that health check is failed
|
|
assertEquals(1, observer.mFailedPackages.size());
|
|
assertEquals(APP_A, observer.mFailedPackages.get(0));
|
|
|
|
// Then clear failed packages and start observing a random package so requests are synced
|
|
// and PackageWatchdog#onSupportedPackages is called and APP_A has a chance to fail again
|
|
// this time due to package expiry.
|
|
observer.mFailedPackages.clear();
|
|
watchdog.startObservingHealth(observer, Arrays.asList(APP_B), LONG_DURATION);
|
|
|
|
// Verify that health check failure is not notified again
|
|
assertTrue(observer.mFailedPackages.isEmpty());
|
|
}
|
|
|
|
/** Tests {@link MonitoredPackage} health check state transitions. */
|
|
@Test
|
|
public void testPackageHealthCheckStateTransitions() {
|
|
MonitoredPackage m1 = new MonitoredPackage(APP_A, LONG_DURATION,
|
|
false /* hasPassedHealthCheck */);
|
|
MonitoredPackage m2 = new MonitoredPackage(APP_B, LONG_DURATION, false);
|
|
MonitoredPackage m3 = new MonitoredPackage(APP_C, LONG_DURATION, false);
|
|
MonitoredPackage m4 = new MonitoredPackage(APP_D, LONG_DURATION, SHORT_DURATION, true);
|
|
|
|
// Verify transition: inactive -> active -> passed
|
|
// Verify initially inactive
|
|
assertEquals(MonitoredPackage.STATE_INACTIVE, m1.getHealthCheckStateLocked());
|
|
// Verify still inactive, until we #setHealthCheckActiveLocked
|
|
assertEquals(MonitoredPackage.STATE_INACTIVE, m1.handleElapsedTimeLocked(SHORT_DURATION));
|
|
// Verify now active
|
|
assertEquals(MonitoredPackage.STATE_ACTIVE, m1.setHealthCheckActiveLocked(SHORT_DURATION));
|
|
// Verify now passed
|
|
assertEquals(MonitoredPackage.STATE_PASSED, m1.tryPassHealthCheckLocked());
|
|
|
|
// Verify transition: inactive -> active -> failed
|
|
// Verify initially inactive
|
|
assertEquals(MonitoredPackage.STATE_INACTIVE, m2.getHealthCheckStateLocked());
|
|
// Verify now active
|
|
assertEquals(MonitoredPackage.STATE_ACTIVE, m2.setHealthCheckActiveLocked(SHORT_DURATION));
|
|
// Verify now failed
|
|
assertEquals(MonitoredPackage.STATE_FAILED, m2.handleElapsedTimeLocked(SHORT_DURATION));
|
|
|
|
// Verify transition: inactive -> failed
|
|
// Verify initially inactive
|
|
assertEquals(MonitoredPackage.STATE_INACTIVE, m3.getHealthCheckStateLocked());
|
|
// Verify now failed because package expired
|
|
assertEquals(MonitoredPackage.STATE_FAILED, m3.handleElapsedTimeLocked(LONG_DURATION));
|
|
// Verify remains failed even when asked to pass
|
|
assertEquals(MonitoredPackage.STATE_FAILED, m3.tryPassHealthCheckLocked());
|
|
|
|
// Verify transition: passed
|
|
assertEquals(MonitoredPackage.STATE_PASSED, m4.getHealthCheckStateLocked());
|
|
// Verify remains passed even if health check fails
|
|
assertEquals(MonitoredPackage.STATE_PASSED, m4.handleElapsedTimeLocked(SHORT_DURATION));
|
|
// Verify remains passed even if package expires
|
|
assertEquals(MonitoredPackage.STATE_PASSED, m4.handleElapsedTimeLocked(LONG_DURATION));
|
|
}
|
|
|
|
private PackageWatchdog createWatchdog() {
|
|
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 {
|
|
private final String mName;
|
|
private int mImpact;
|
|
final List<String> mFailedPackages = new ArrayList<>();
|
|
|
|
TestObserver(String name) {
|
|
mName = name;
|
|
mImpact = PackageHealthObserverImpact.USER_IMPACT_MEDIUM;
|
|
}
|
|
|
|
TestObserver(String name, int impact) {
|
|
mName = name;
|
|
mImpact = impact;
|
|
}
|
|
|
|
public int onHealthCheckFailed(VersionedPackage versionedPackage) {
|
|
return mImpact;
|
|
}
|
|
|
|
public boolean execute(VersionedPackage versionedPackage) {
|
|
mFailedPackages.add(versionedPackage.getPackageName());
|
|
return true;
|
|
}
|
|
|
|
public String getName() {
|
|
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 Consumer<String> mPassedConsumer;
|
|
private Consumer<List<PackageInfo>> mSupportedConsumer;
|
|
private Runnable mNotifySyncRunnable;
|
|
|
|
@Override
|
|
public void setEnabled(boolean enabled) {
|
|
mIsEnabled = enabled;
|
|
if (!mIsEnabled) {
|
|
mSupportedPackages.clear();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setCallbacks(Consumer<String> passedConsumer,
|
|
Consumer<List<PackageInfo>> supportedConsumer, Runnable notifySyncRunnable) {
|
|
mPassedConsumer = passedConsumer;
|
|
mSupportedConsumer = supportedConsumer;
|
|
mNotifySyncRunnable = notifySyncRunnable;
|
|
}
|
|
|
|
@Override
|
|
public void syncRequests(Set<String> packages) {
|
|
mRequestedPackages.clear();
|
|
if (mIsEnabled) {
|
|
packages.retainAll(mSupportedPackages);
|
|
mRequestedPackages.addAll(packages);
|
|
List<PackageInfo> packageInfos = new ArrayList<>();
|
|
for (String packageName: packages) {
|
|
packageInfos.add(new PackageInfo(packageName, SHORT_DURATION));
|
|
}
|
|
mSupportedConsumer.accept(packageInfos);
|
|
} else {
|
|
mSupportedConsumer.accept(Collections.emptyList());
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|