/* * 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.provider.TimeZoneRulesDataContract; import android.util.Slog; /** * 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 implements IntentHelper.Listener { 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; // 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); return new PackageTracker( helperImpl /* clock */, helperImpl /* configHelper */, helperImpl /* packageManagerHelper */, new PackageStatusStorage(context), 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(); // 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); // Enable the reliability triggering so we will have at least one reliability trigger if // a package isn't updated. mIntentHelper.enableReliabilityTriggering(); 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. // TODO(nfuller) Uncomment or remove the code below. Currently an app stops being a priv-app // after it is replaced by one in data so this check fails. http://b/35995024 // try { // if (!mPackageManagerHelper.isPrivilegedApp(mUpdateAppPackageName)) { // throw failWithException( // "Update app " + mUpdateAppPackageName + " must be a priv-app.", null); // } // } catch (PackageManager.NameNotFoundException e) { // throw failWithException("Could not determine update app package details for " // + mUpdateAppPackageName, e); // } // TODO(nfuller) Consider permission checks. While an updated system app retains permissions // obtained by the system version it's not clear how to check them. Slog.d(TAG, "Update app " + mUpdateAppPackageName + " is valid."); // Validate the data application package. // TODO(nfuller) Uncomment or remove the code below. Currently an app stops being a priv-app // after it is replaced by one in data. http://b/35995024 // try { // if (!mPackageManagerHelper.isPrivilegedApp(mDataAppPackageName)) { // throw failWithException( // "Data app " + mDataAppPackageName + " must be a priv-app.", null); // } // } catch (PackageManager.NameNotFoundException e) { // throw failWithException("Could not determine data app package details for " // + mDataAppPackageName, e); // } // TODO(nfuller) Consider permission checks. While an updated system app retains permissions // obtained by the system version it's not clear how to check them. 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 */ @Override 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 reliability checks if the current packages are bad. mIntentHelper.disableReliabilityTriggering(); 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."); // Not doing any work, but also not disabling future reliability triggers. 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.disableReliabilityTriggering(); return; } else if (mCheckFailureCount == 0) { // Case 4. Slog.i(TAG, "triggerUpdateIfNeeded: No reliability check required. Last check was" + " successful."); mIntentHelper.disableReliabilityTriggering(); 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.disableReliabilityTriggering(); 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.disableReliabilityTriggering(); 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."); return; } // Trigger the update check. mIntentHelper.sendTriggerUpdateCheck(checkToken); mCheckTriggered = true; // Update the reliability check state in case the update fails. setCheckInProgress(); // Enable reliability triggering in case the check doesn't succeed and there is no // response at all. Enabling reliability triggering is idempotent. mIntentHelper.enableReliabilityTriggering(); } /** * 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(); // Enable reliability triggering and reset the failure count so we know that the // next reliability trigger will do something. mIntentHelper.enableReliabilityTriggering(); 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 more reliability checks are required until // there is a package change. mIntentHelper.disableReliabilityTriggering(); mCheckFailureCount = 0; } else { // Enable reliability triggering to potentially check again in future. mIntentHelper.enableReliabilityTriggering(); 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"); // Enable reliability triggering to potentially try again in future. mIntentHelper.enableReliabilityTriggering(); 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; } // TODO(nfuller) Add any permissions checks needed. 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); } }