blob: 8f4cada0d0ba25ff19d2bec14179257875c8bb7b [file] [log] [blame]
/*
* Copyright (C) 2017 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.timezone;
import com.android.internal.annotations.VisibleForTesting;
import android.app.timezone.RulesUpdaterContract;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.os.FileUtils;
import android.os.SystemClock;
import android.provider.TimeZoneRulesDataContract;
import android.util.Slog;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.Clock;
/**
* Monitors the installed applications associated with time zone updates. If the app packages are
* updated it indicates there <em>might</em> be a time zone rules update to apply so a targeted
* broadcast intent is used to trigger the time zone updater app.
*
* <p>The "update triggering" behavior of this component can be disabled via device configuration.
*
* <p>The package tracker listens for package updates of the time zone "updater app" and "data app".
* It also listens for "reliability" triggers. Reliability triggers are there to ensure that the
* package tracker handles failures reliably and are "idle maintenance" events or something similar.
* Reliability triggers can cause a time zone update check to take place if the current state is
* unclear. For example, it can be unclear after boot or after a failure. If there are repeated
* failures reliability updates are halted until the next boot.
*
* <p>This component keeps persistent track of the most recent app packages checked to avoid
* unnecessary expense from broadcasting intents (which will cause other app processes to spawn).
* The current status is also stored to detect whether the most recently-generated check is
* complete successfully. For example, if the device was interrupted while doing a check and never
* acknowledged a check then a check will be retried the next time a "reliability trigger" event
* happens.
*/
// Also made non-final so it can be mocked.
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public class PackageTracker {
private static final String TAG = "timezone.PackageTracker";
private final PackageManagerHelper mPackageManagerHelper;
private final PackageTrackerIntentHelper mIntentHelper;
private final ConfigHelper mConfigHelper;
private final PackageStatusStorage mPackageStatusStorage;
private final Clock mElapsedRealtimeClock;
// False if tracking is disabled.
private boolean mTrackingEnabled;
// These fields may be null if package tracking is disabled.
private String mUpdateAppPackageName;
private String mDataAppPackageName;
// The time a triggered check is allowed to take before it is considered overdue.
private int mCheckTimeAllowedMillis;
// The number of failed checks in a row before reliability checks should stop happening.
private long mFailedCheckRetryCount;
/*
* The minimum delay between a successive reliability triggers / other operations. Should to be
* larger than mCheckTimeAllowedMillis to avoid reliability triggers happening during package
* update checks.
*/
private int mDelayBeforeReliabilityCheckMillis;
// Reliability check state: If a check was triggered but not acknowledged within
// mCheckTimeAllowedMillis then another one can be triggered.
private Long mLastTriggerTimestamp = null;
// Reliability check state: Whether any checks have been triggered at all.
private boolean mCheckTriggered;
// Reliability check state: A count of how many failures have occurred consecutively.
private int mCheckFailureCount;
/** Creates the {@link PackageTracker} for normal use. */
static PackageTracker create(Context context) {
Clock elapsedRealtimeClock = SystemClock.elapsedRealtimeClock();
PackageTrackerHelperImpl helperImpl = new PackageTrackerHelperImpl(context);
File storageDir = FileUtils.createDir(Environment.getDataSystemDirectory(), "timezone");
return new PackageTracker(
elapsedRealtimeClock /* elapsedRealtimeClock */,
helperImpl /* configHelper */,
helperImpl /* packageManagerHelper */,
new PackageStatusStorage(storageDir),
new PackageTrackerIntentHelperImpl(context));
}
// A constructor that can be used by tests to supply mocked / faked dependencies.
PackageTracker(Clock elapsedRealtimeClock, ConfigHelper configHelper,
PackageManagerHelper packageManagerHelper, PackageStatusStorage packageStatusStorage,
PackageTrackerIntentHelper intentHelper) {
mElapsedRealtimeClock = elapsedRealtimeClock;
mConfigHelper = configHelper;
mPackageManagerHelper = packageManagerHelper;
mPackageStatusStorage = packageStatusStorage;
mIntentHelper = intentHelper;
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
protected synchronized boolean start() {
mTrackingEnabled = mConfigHelper.isTrackingEnabled();
if (!mTrackingEnabled) {
Slog.i(TAG, "Time zone updater / data package tracking explicitly disabled.");
return false;
}
mUpdateAppPackageName = mConfigHelper.getUpdateAppPackageName();
mDataAppPackageName = mConfigHelper.getDataAppPackageName();
mCheckTimeAllowedMillis = mConfigHelper.getCheckTimeAllowedMillis();
mFailedCheckRetryCount = mConfigHelper.getFailedCheckRetryCount();
mDelayBeforeReliabilityCheckMillis = mCheckTimeAllowedMillis + (60 * 1000);
// Validate the device configuration including the application packages.
// The manifest entries in the apps themselves are not validated until use as they can
// change and we don't want to prevent the system server starting due to a bad application.
throwIfDeviceSettingsOrAppsAreBad();
// Explicitly start in a reliability state where reliability triggering will do something.
mCheckTriggered = false;
mCheckFailureCount = 0;
// Initialize the storage, as needed.
try {
mPackageStatusStorage.initialize();
} catch (IOException e) {
Slog.w(TAG, "PackageTracker storage could not be initialized.", e);
return false;
}
// Initialize the intent helper.
mIntentHelper.initialize(mUpdateAppPackageName, mDataAppPackageName, this);
// Schedule a reliability trigger so we will have at least one after boot. This will allow
// us to catch if a package updated wasn't handled to completion. There's no hurry: it's ok
// to delay for a while before doing this even if idle.
mIntentHelper.scheduleReliabilityTrigger(mDelayBeforeReliabilityCheckMillis);
Slog.i(TAG, "Time zone updater / data package tracking enabled");
return true;
}
/**
* Performs checks that confirm the system image has correctly configured package
* tracking configuration. Only called if package tracking is enabled. Throws an exception if
* the device is configured badly which will prevent the device booting.
*/
private void throwIfDeviceSettingsOrAppsAreBad() {
// None of the checks below can be based on application manifest settings, otherwise a bad
// update could leave the device in an unbootable state. See validateDataAppManifest() and
// validateUpdaterAppManifest() for softer errors.
throwRuntimeExceptionIfNullOrEmpty(
mUpdateAppPackageName, "Update app package name missing.");
throwRuntimeExceptionIfNullOrEmpty(mDataAppPackageName, "Data app package name missing.");
if (mFailedCheckRetryCount < 1) {
throw logAndThrowRuntimeException("mFailedRetryCount=" + mFailedCheckRetryCount, null);
}
if (mCheckTimeAllowedMillis < 1000) {
throw logAndThrowRuntimeException(
"mCheckTimeAllowedMillis=" + mCheckTimeAllowedMillis, null);
}
// Validate the updater application package.
try {
if (!mPackageManagerHelper.isPrivilegedApp(mUpdateAppPackageName)) {
throw logAndThrowRuntimeException(
"Update app " + mUpdateAppPackageName + " must be a priv-app.", null);
}
} catch (PackageManager.NameNotFoundException e) {
throw logAndThrowRuntimeException("Could not determine update app package details for "
+ mUpdateAppPackageName, e);
}
Slog.d(TAG, "Update app " + mUpdateAppPackageName + " is valid.");
// Validate the data application package.
try {
if (!mPackageManagerHelper.isPrivilegedApp(mDataAppPackageName)) {
throw logAndThrowRuntimeException(
"Data app " + mDataAppPackageName + " must be a priv-app.", null);
}
} catch (PackageManager.NameNotFoundException e) {
throw logAndThrowRuntimeException("Could not determine data app package details for "
+ mDataAppPackageName, e);
}
Slog.d(TAG, "Data app " + mDataAppPackageName + " is valid.");
}
/**
* Inspects the current in-memory state, installed packages and storage state to determine if an
* update check is needed and then trigger if it is.
*
* @param packageChanged true if this method was called because a known packaged definitely
* changed, false if the cause is a reliability trigger
*/
public synchronized void triggerUpdateIfNeeded(boolean packageChanged) {
if (!mTrackingEnabled) {
throw new IllegalStateException("Unexpected call. Tracking is disabled.");
}
// Validate the applications' current manifest entries: make sure they are configured as
// they should be. These are not fatal and just means that no update is triggered: we don't
// want to take down the system server if an OEM or Google have pushed a bad update to
// an application.
boolean updaterAppManifestValid = validateUpdaterAppManifest();
boolean dataAppManifestValid = validateDataAppManifest();
if (!updaterAppManifestValid || !dataAppManifestValid) {
Slog.e(TAG, "No update triggered due to invalid application manifest entries."
+ " updaterApp=" + updaterAppManifestValid
+ ", dataApp=" + dataAppManifestValid);
// There's no point in doing any reliability triggers if the current packages are bad.
mIntentHelper.unscheduleReliabilityTrigger();
return;
}
if (!packageChanged) {
// This call was made because the device is doing a "reliability" check.
// 4 possible cases:
// 1) No check has previously triggered since restart. We want to trigger in this case.
// 2) A check has previously triggered and it is in progress. We want to trigger if
// the response is overdue.
// 3) A check has previously triggered and it failed. We want to trigger, but only if
// we're not in a persistent failure state.
// 4) A check has previously triggered and it succeeded.
// We don't want to trigger, and want to stop future triggers.
if (!mCheckTriggered) {
// Case 1.
Slog.d(TAG, "triggerUpdateIfNeeded: First reliability trigger.");
} else if (isCheckInProgress()) {
// Case 2.
if (!isCheckResponseOverdue()) {
// A check is in progress but hasn't been given time to succeed.
Slog.d(TAG,
"triggerUpdateIfNeeded: checkComplete call is not yet overdue."
+ " Not triggering.");
// Don't do any work now but we do schedule a future reliability trigger.
mIntentHelper.scheduleReliabilityTrigger(mDelayBeforeReliabilityCheckMillis);
return;
}
} else if (mCheckFailureCount > mFailedCheckRetryCount) {
// Case 3. If the system is in some kind of persistent failure state we don't want
// to keep checking, so just stop.
Slog.i(TAG, "triggerUpdateIfNeeded: number of allowed consecutive check failures"
+ " exceeded. Stopping reliability triggers until next reboot or package"
+ " update.");
mIntentHelper.unscheduleReliabilityTrigger();
return;
} else if (mCheckFailureCount == 0) {
// Case 4.
Slog.i(TAG, "triggerUpdateIfNeeded: No reliability check required. Last check was"
+ " successful.");
mIntentHelper.unscheduleReliabilityTrigger();
return;
}
}
// Read the currently installed data / updater package versions.
PackageVersions currentInstalledVersions = lookupInstalledPackageVersions();
if (currentInstalledVersions == null) {
// This should not happen if the device is configured in a valid way.
Slog.e(TAG, "triggerUpdateIfNeeded: currentInstalledVersions was null");
mIntentHelper.unscheduleReliabilityTrigger();
return;
}
// Establish the current state using package manager and stored state. Determine if we have
// already successfully checked the installed versions.
PackageStatus packageStatus = mPackageStatusStorage.getPackageStatus();
if (packageStatus == null) {
// This can imply corrupt, uninitialized storage state (e.g. first check ever on a
// device) or after some kind of reset.
Slog.i(TAG, "triggerUpdateIfNeeded: No package status data found. Data check needed.");
} else if (!packageStatus.mVersions.equals(currentInstalledVersions)) {
// The stored package version information differs from the installed version.
// Trigger the check in all cases.
Slog.i(TAG, "triggerUpdateIfNeeded: Stored package versions="
+ packageStatus.mVersions + ", do not match current package versions="
+ currentInstalledVersions + ". Triggering check.");
} else {
Slog.i(TAG, "triggerUpdateIfNeeded: Stored package versions match currently"
+ " installed versions, currentInstalledVersions=" + currentInstalledVersions
+ ", packageStatus.mCheckStatus=" + packageStatus.mCheckStatus);
if (packageStatus.mCheckStatus == PackageStatus.CHECK_COMPLETED_SUCCESS) {
// The last check succeeded and nothing has changed. Do nothing and disable
// reliability checks.
Slog.i(TAG, "triggerUpdateIfNeeded: Prior check succeeded. No need to trigger.");
mIntentHelper.unscheduleReliabilityTrigger();
return;
}
}
// Generate a token to send to the updater app.
CheckToken checkToken =
mPackageStatusStorage.generateCheckToken(currentInstalledVersions);
if (checkToken == null) {
Slog.w(TAG, "triggerUpdateIfNeeded: Unable to generate check token."
+ " Not sending check request.");
// Trigger again later: perhaps we'll have better luck.
mIntentHelper.scheduleReliabilityTrigger(mDelayBeforeReliabilityCheckMillis);
return;
}
// Trigger the update check.
mIntentHelper.sendTriggerUpdateCheck(checkToken);
mCheckTriggered = true;
// Update the reliability check state in case the update fails.
setCheckInProgress();
// Schedule a reliability trigger in case the update check doesn't succeed and there is no
// response at all. It will be cancelled if the check is successful in recordCheckResult.
mIntentHelper.scheduleReliabilityTrigger(mDelayBeforeReliabilityCheckMillis);
}
/**
* Used to record the result of a check. Can be called even if active package tracking is
* disabled.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
protected synchronized void recordCheckResult(CheckToken checkToken, boolean success) {
Slog.i(TAG, "recordOperationResult: checkToken=" + checkToken + " success=" + success);
// If package tracking is disabled it means no record-keeping is required. However, we do
// want to clear out any stored state to make it clear that the current state is unknown and
// should tracking become enabled again (perhaps through an OTA) we'd need to perform an
// update check.
if (!mTrackingEnabled) {
// This means an updater has spontaneously modified time zone data without having been
// triggered. This can happen if the OEM is handling their own updates, but we don't
// need to do any tracking in this case.
if (checkToken == null) {
// This is the expected case if tracking is disabled but an OEM is handling time
// zone installs using their own mechanism.
Slog.d(TAG, "recordCheckResult: Tracking is disabled and no token has been"
+ " provided. Resetting tracking state.");
} else {
// This is unexpected. If tracking is disabled then no check token should have been
// generated by the package tracker. An updater should never create its own token.
// This could be a bug in the updater.
Slog.w(TAG, "recordCheckResult: Tracking is disabled and a token " + checkToken
+ " has been unexpectedly provided. Resetting tracking state.");
}
mPackageStatusStorage.resetCheckState();
return;
}
if (checkToken == null) {
/*
* If the checkToken is null it suggests an install / uninstall / acknowledgement has
* occurred without a prior trigger (or the client didn't return the token it was given
* for some reason, perhaps a bug).
*
* This shouldn't happen under normal circumstances:
*
* If package tracking is enabled, we assume it is the package tracker responsible for
* triggering updates and a token should have been produced and returned.
*
* If the OEM is handling time zone updates case package tracking should be disabled.
*
* This could happen in tests. The device should recover back to a known state by
* itself rather than be left in an invalid state.
*
* We treat this as putting the device into an unknown state and make sure that
* reliability triggering is enabled so we should recover.
*/
Slog.i(TAG, "recordCheckResult: Unexpectedly missing checkToken, resetting"
+ " storage state.");
mPackageStatusStorage.resetCheckState();
// Schedule a reliability trigger and reset the failure count so we know that the
// next reliability trigger will do something.
mIntentHelper.scheduleReliabilityTrigger(mDelayBeforeReliabilityCheckMillis);
mCheckFailureCount = 0;
} else {
// This is the expected case when tracking is enabled: a check was triggered and it has
// completed.
boolean recordedCheckCompleteSuccessfully =
mPackageStatusStorage.markChecked(checkToken, success);
if (recordedCheckCompleteSuccessfully) {
// If we have recorded the result (whatever it was) we know there is no check in
// progress.
setCheckComplete();
if (success) {
// Since the check was successful, no reliability trigger is required until
// there is a package change.
mIntentHelper.unscheduleReliabilityTrigger();
mCheckFailureCount = 0;
} else {
// Enable schedule a reliability trigger to check again in future.
mIntentHelper.scheduleReliabilityTrigger(mDelayBeforeReliabilityCheckMillis);
mCheckFailureCount++;
}
} else {
// The failure to record the check means an optimistic lock failure and suggests
// that another check was triggered after the token was generated.
Slog.i(TAG, "recordCheckResult: could not update token=" + checkToken
+ " with success=" + success + ". Optimistic lock failure");
// Schedule a reliability trigger to potentially try again in future.
mIntentHelper.scheduleReliabilityTrigger(mDelayBeforeReliabilityCheckMillis);
mCheckFailureCount++;
}
}
}
/** Access to consecutive failure counts for use in tests. */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
protected int getCheckFailureCountForTests() {
return mCheckFailureCount;
}
private void setCheckInProgress() {
mLastTriggerTimestamp = mElapsedRealtimeClock.millis();
}
private void setCheckComplete() {
mLastTriggerTimestamp = null;
}
private boolean isCheckInProgress() {
return mLastTriggerTimestamp != null;
}
private boolean isCheckResponseOverdue() {
if (mLastTriggerTimestamp == null) {
return false;
}
// Risk of overflow, but highly unlikely given the implementation and not problematic.
return mElapsedRealtimeClock.millis() > mLastTriggerTimestamp + mCheckTimeAllowedMillis;
}
private PackageVersions lookupInstalledPackageVersions() {
long updatePackageVersion;
long dataPackageVersion;
try {
updatePackageVersion =
mPackageManagerHelper.getInstalledPackageVersion(mUpdateAppPackageName);
dataPackageVersion =
mPackageManagerHelper.getInstalledPackageVersion(mDataAppPackageName);
} catch (PackageManager.NameNotFoundException e) {
Slog.w(TAG, "lookupInstalledPackageVersions: Unable to resolve installed package"
+ " versions", e);
return null;
}
return new PackageVersions(updatePackageVersion, dataPackageVersion);
}
private boolean validateDataAppManifest() {
// We only want to talk to a provider that exposed by the known data app package
// so we look up the providers exposed by that app and check the well-known authority is
// there. This prevents the case where *even if* the data app doesn't expose the provider
// required, another app cannot expose one to replace it.
if (!mPackageManagerHelper.contentProviderRegistered(
TimeZoneRulesDataContract.AUTHORITY, mDataAppPackageName)) {
// Error! Found the package but it didn't expose the correct provider.
Slog.w(TAG, "validateDataAppManifest: Data app " + mDataAppPackageName
+ " does not expose the required provider with authority="
+ TimeZoneRulesDataContract.AUTHORITY);
return false;
}
return true;
}
private boolean validateUpdaterAppManifest() {
try {
// The updater app is expected to have the UPDATE_TIME_ZONE_RULES permission.
// The updater app is expected to have a receiver for the intent we are going to trigger
// and require the TRIGGER_TIME_ZONE_RULES_CHECK.
if (!mPackageManagerHelper.usesPermission(
mUpdateAppPackageName,
RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION)) {
Slog.w(TAG, "validateUpdaterAppManifest: Updater app " + mDataAppPackageName
+ " does not use permission="
+ RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION);
return false;
}
if (!mPackageManagerHelper.receiverRegistered(
RulesUpdaterContract.createUpdaterIntent(mUpdateAppPackageName),
RulesUpdaterContract.TRIGGER_TIME_ZONE_RULES_CHECK_PERMISSION)) {
return false;
}
return true;
} catch (PackageManager.NameNotFoundException e) {
Slog.w(TAG, "validateUpdaterAppManifest: Updater app " + mDataAppPackageName
+ " does not expose the required broadcast receiver.", e);
return false;
}
}
private static void throwRuntimeExceptionIfNullOrEmpty(String value, String message) {
if (value == null || value.trim().isEmpty()) {
throw logAndThrowRuntimeException(message, null);
}
}
private static RuntimeException logAndThrowRuntimeException(String message, Throwable cause) {
Slog.wtf(TAG, message, cause);
throw new RuntimeException(message, cause);
}
public void dump(PrintWriter fout) {
fout.println("PackageTrackerState: " + toString());
mPackageStatusStorage.dump(fout);
}
@Override
public String toString() {
return "PackageTracker{" +
"mTrackingEnabled=" + mTrackingEnabled +
", mUpdateAppPackageName='" + mUpdateAppPackageName + '\'' +
", mDataAppPackageName='" + mDataAppPackageName + '\'' +
", mCheckTimeAllowedMillis=" + mCheckTimeAllowedMillis +
", mDelayBeforeReliabilityCheckMillis=" + mDelayBeforeReliabilityCheckMillis +
", mFailedCheckRetryCount=" + mFailedCheckRetryCount +
", mLastTriggerTimestamp=" + mLastTriggerTimestamp +
", mCheckTriggered=" + mCheckTriggered +
", mCheckFailureCount=" + mCheckFailureCount +
'}';
}
}