/* * 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.provider.TimeZoneRulesDataContract; import android.util.Slog; import java.io.File; import java.io.PrintWriter; /** * Monitors the installed applications associated with time zone updates. If the app packages are * updated it indicates there might be a time zone rules update to apply so a targeted * broadcast intent is used to trigger the time zone updater app. * *
The "update triggering" behavior of this component can be disabled via device configuration. * *
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. * *
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 IntentHelper mIntentHelper; private final ConfigHelper mConfigHelper; private final PackageStatusStorage mPackageStatusStorage; private final ClockHelper mClockHelper; // 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) { PackageTrackerHelperImpl helperImpl = new PackageTrackerHelperImpl(context); // TODO(nfuller): Switch to FileUtils.createDir() when available. http://b/31008728 File storageDir = new File(Environment.getDataSystemDirectory(), "timezone"); if (!storageDir.exists()) { storageDir.mkdir(); } return new PackageTracker( helperImpl /* clock */, helperImpl /* configHelper */, helperImpl /* packageManagerHelper */, new PackageStatusStorage(storageDir), new IntentHelperImpl(context)); } // A constructor that can be used by tests to supply mocked / faked dependencies. PackageTracker(ClockHelper clockHelper, ConfigHelper configHelper, PackageManagerHelper packageManagerHelper, PackageStatusStorage packageStatusStorage, IntentHelper intentHelper) { mClockHelper = clockHelper; mConfigHelper = configHelper; mPackageManagerHelper = packageManagerHelper; mPackageStatusStorage = packageStatusStorage; mIntentHelper = intentHelper; } @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) protected synchronized void start() { mTrackingEnabled = mConfigHelper.isTrackingEnabled(); if (!mTrackingEnabled) { Slog.i(TAG, "Time zone updater / data package tracking explicitly disabled."); return; } 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 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"); } /** * 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 = mClockHelper.currentTimestamp(); } 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 mClockHelper.currentTimestamp() > mLastTriggerTimestamp + mCheckTimeAllowedMillis; } private PackageVersions lookupInstalledPackageVersions() { int updatePackageVersion; int 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 + '}'; } }