| /* |
| * 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; |
| |
| /** |
| * 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); |
| // 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(); |
| |
| // 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); |
| } |
| } |