| /* |
| * 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.GuardedBy; |
| import com.android.internal.util.FastXmlSerializer; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| import org.xmlpull.v1.XmlSerializer; |
| |
| import android.util.AtomicFile; |
| import android.util.Slog; |
| import android.util.Xml; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.nio.charset.StandardCharsets; |
| import java.text.ParseException; |
| import java.io.PrintWriter; |
| |
| import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_FAILURE; |
| import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_SUCCESS; |
| import static com.android.server.timezone.PackageStatus.CHECK_STARTED; |
| import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; |
| import static org.xmlpull.v1.XmlPullParser.START_TAG; |
| |
| /** |
| * Storage logic for accessing/mutating the Android system's persistent state related to time zone |
| * update checking. There is expected to be a single instance. All non-private methods are thread |
| * safe. |
| */ |
| final class PackageStatusStorage { |
| |
| private static final String LOG_TAG = "timezone.PackageStatusStorage"; |
| |
| private static final String TAG_PACKAGE_STATUS = "PackageStatus"; |
| |
| /** |
| * Attribute that stores a monotonically increasing lock ID, used to detect concurrent update |
| * issues without on-line locks. Incremented on every write. |
| */ |
| private static final String ATTRIBUTE_OPTIMISTIC_LOCK_ID = "optimisticLockId"; |
| |
| /** |
| * Attribute that stores the current "check status" of the time zone update application |
| * packages. |
| */ |
| private static final String ATTRIBUTE_CHECK_STATUS = "checkStatus"; |
| |
| /** |
| * Attribute that stores the version of the time zone rules update application being checked |
| * / last checked. |
| */ |
| private static final String ATTRIBUTE_UPDATE_APP_VERSION = "updateAppPackageVersion"; |
| |
| /** |
| * Attribute that stores the version of the time zone rules data application being checked |
| * / last checked. |
| */ |
| private static final String ATTRIBUTE_DATA_APP_VERSION = "dataAppPackageVersion"; |
| |
| private static final int UNKNOWN_PACKAGE_VERSION = -1; |
| |
| private final AtomicFile mPackageStatusFile; |
| |
| PackageStatusStorage(File storageDir) { |
| mPackageStatusFile = new AtomicFile(new File(storageDir, "package-status.xml")); |
| if (!mPackageStatusFile.getBaseFile().exists()) { |
| try { |
| insertInitialPackageStatus(); |
| } catch (IOException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| } |
| |
| void deleteFileForTests() { |
| synchronized(this) { |
| mPackageStatusFile.delete(); |
| } |
| } |
| |
| /** |
| * Obtain the current check status of the application packages. Returns {@code null} the first |
| * time it is called, or after {@link #resetCheckState()}. |
| */ |
| PackageStatus getPackageStatus() { |
| synchronized (this) { |
| try { |
| return getPackageStatusLocked(); |
| } catch (ParseException e) { |
| // This means that data exists in the file but it was bad. |
| Slog.e(LOG_TAG, "Package status invalid, resetting and retrying", e); |
| |
| // Reset the storage so it is in a good state again. |
| recoverFromBadData(e); |
| try { |
| return getPackageStatusLocked(); |
| } catch (ParseException e2) { |
| throw new IllegalStateException("Recovery from bad file failed", e2); |
| } |
| } |
| } |
| } |
| |
| @GuardedBy("this") |
| private PackageStatus getPackageStatusLocked() throws ParseException { |
| try (FileInputStream fis = mPackageStatusFile.openRead()) { |
| XmlPullParser parser = parseToPackageStatusTag(fis); |
| Integer checkStatus = getNullableIntAttribute(parser, ATTRIBUTE_CHECK_STATUS); |
| if (checkStatus == null) { |
| return null; |
| } |
| int updateAppVersion = getIntAttribute(parser, ATTRIBUTE_UPDATE_APP_VERSION); |
| int dataAppVersion = getIntAttribute(parser, ATTRIBUTE_DATA_APP_VERSION); |
| return new PackageStatus(checkStatus, |
| new PackageVersions(updateAppVersion, dataAppVersion)); |
| } catch (IOException e) { |
| ParseException e2 = new ParseException("Error reading package status", 0); |
| e2.initCause(e); |
| throw e2; |
| } |
| } |
| |
| @GuardedBy("this") |
| private int recoverFromBadData(Exception cause) { |
| mPackageStatusFile.delete(); |
| try { |
| return insertInitialPackageStatus(); |
| } catch (IOException e) { |
| IllegalStateException fatal = new IllegalStateException(e); |
| fatal.addSuppressed(cause); |
| throw fatal; |
| } |
| } |
| |
| /** Insert the initial data, returning the optimistic lock ID */ |
| private int insertInitialPackageStatus() throws IOException { |
| // Doesn't matter what it is, but we avoid the obvious starting value each time the data |
| // is reset to ensure that old tokens are unlikely to work. |
| final int initialOptimisticLockId = (int) System.currentTimeMillis(); |
| |
| writePackageStatusLocked(null /* status */, initialOptimisticLockId, |
| null /* packageVersions */); |
| return initialOptimisticLockId; |
| } |
| |
| /** |
| * Generate a new {@link CheckToken} that can be passed to the time zone rules update |
| * application. |
| */ |
| CheckToken generateCheckToken(PackageVersions currentInstalledVersions) { |
| if (currentInstalledVersions == null) { |
| throw new NullPointerException("currentInstalledVersions == null"); |
| } |
| |
| synchronized (this) { |
| int optimisticLockId; |
| try { |
| optimisticLockId = getCurrentOptimisticLockId(); |
| } catch (ParseException e) { |
| Slog.w(LOG_TAG, "Unable to find optimistic lock ID from package status"); |
| |
| // Recover. |
| optimisticLockId = recoverFromBadData(e); |
| } |
| |
| int newOptimisticLockId = optimisticLockId + 1; |
| try { |
| boolean statusUpdated = writePackageStatusWithOptimisticLockCheck( |
| optimisticLockId, newOptimisticLockId, CHECK_STARTED, |
| currentInstalledVersions); |
| if (!statusUpdated) { |
| throw new IllegalStateException("Unable to update status to CHECK_STARTED." |
| + " synchronization failure?"); |
| } |
| return new CheckToken(newOptimisticLockId, currentInstalledVersions); |
| } catch (IOException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| } |
| |
| /** |
| * Reset the current device state to "unknown". |
| */ |
| void resetCheckState() { |
| synchronized(this) { |
| int optimisticLockId; |
| try { |
| optimisticLockId = getCurrentOptimisticLockId(); |
| } catch (ParseException e) { |
| Slog.w(LOG_TAG, "resetCheckState: Unable to find optimistic lock ID from package" |
| + " status"); |
| // Attempt to recover the storage state. |
| optimisticLockId = recoverFromBadData(e); |
| } |
| |
| int newOptimisticLockId = optimisticLockId + 1; |
| try { |
| if (!writePackageStatusWithOptimisticLockCheck(optimisticLockId, |
| newOptimisticLockId, null /* status */, null /* packageVersions */)) { |
| throw new IllegalStateException("resetCheckState: Unable to reset package" |
| + " status, newOptimisticLockId=" + newOptimisticLockId); |
| } |
| } catch (IOException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| } |
| |
| /** |
| * Update the current device state if possible. Returns true if the update was successful. |
| * {@code false} indicates the storage has been changed since the {@link CheckToken} was |
| * generated and the update was discarded. |
| */ |
| boolean markChecked(CheckToken checkToken, boolean succeeded) { |
| synchronized (this) { |
| int optimisticLockId = checkToken.mOptimisticLockId; |
| int newOptimisticLockId = optimisticLockId + 1; |
| int status = succeeded ? CHECK_COMPLETED_SUCCESS : CHECK_COMPLETED_FAILURE; |
| try { |
| return writePackageStatusWithOptimisticLockCheck(optimisticLockId, |
| newOptimisticLockId, status, checkToken.mPackageVersions); |
| } catch (IOException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| } |
| |
| @GuardedBy("this") |
| private int getCurrentOptimisticLockId() throws ParseException { |
| try (FileInputStream fis = mPackageStatusFile.openRead()) { |
| XmlPullParser parser = parseToPackageStatusTag(fis); |
| return getIntAttribute(parser, ATTRIBUTE_OPTIMISTIC_LOCK_ID); |
| } catch (IOException e) { |
| ParseException e2 = new ParseException("Unable to read file", 0); |
| e2.initCause(e); |
| throw e2; |
| } |
| } |
| |
| /** Returns a parser or throws ParseException, never returns null. */ |
| private static XmlPullParser parseToPackageStatusTag(FileInputStream fis) |
| throws ParseException { |
| try { |
| XmlPullParser parser = Xml.newPullParser(); |
| parser.setInput(fis, StandardCharsets.UTF_8.name()); |
| int type; |
| while ((type = parser.next()) != END_DOCUMENT) { |
| final String tag = parser.getName(); |
| if (type == START_TAG && TAG_PACKAGE_STATUS.equals(tag)) { |
| return parser; |
| } |
| } |
| throw new ParseException("Unable to find " + TAG_PACKAGE_STATUS + " tag", 0); |
| } catch (XmlPullParserException e) { |
| throw new IllegalStateException("Unable to configure parser", e); |
| } catch (IOException e) { |
| ParseException e2 = new ParseException("Error reading XML", 0); |
| e.initCause(e); |
| throw e2; |
| } |
| } |
| |
| @GuardedBy("this") |
| private boolean writePackageStatusWithOptimisticLockCheck(int optimisticLockId, |
| int newOptimisticLockId, Integer status, PackageVersions packageVersions) |
| throws IOException { |
| |
| int currentOptimisticLockId; |
| try { |
| currentOptimisticLockId = getCurrentOptimisticLockId(); |
| if (currentOptimisticLockId != optimisticLockId) { |
| return false; |
| } |
| } catch (ParseException e) { |
| recoverFromBadData(e); |
| return false; |
| } |
| |
| writePackageStatusLocked(status, newOptimisticLockId, packageVersions); |
| return true; |
| } |
| |
| @GuardedBy("this") |
| private void writePackageStatusLocked(Integer status, int optimisticLockId, |
| PackageVersions packageVersions) throws IOException { |
| if ((status == null) != (packageVersions == null)) { |
| throw new IllegalArgumentException( |
| "Provide both status and packageVersions, or neither."); |
| } |
| |
| FileOutputStream fos = null; |
| try { |
| fos = mPackageStatusFile.startWrite(); |
| XmlSerializer serializer = new FastXmlSerializer(); |
| serializer.setOutput(fos, StandardCharsets.UTF_8.name()); |
| serializer.startDocument(null /* encoding */, true /* standalone */); |
| final String namespace = null; |
| serializer.startTag(namespace, TAG_PACKAGE_STATUS); |
| String statusAttributeValue = status == null ? "" : Integer.toString(status); |
| serializer.attribute(namespace, ATTRIBUTE_CHECK_STATUS, statusAttributeValue); |
| serializer.attribute(namespace, ATTRIBUTE_OPTIMISTIC_LOCK_ID, |
| Integer.toString(optimisticLockId)); |
| int updateAppVersion = status == null |
| ? UNKNOWN_PACKAGE_VERSION : packageVersions.mUpdateAppVersion; |
| serializer.attribute(namespace, ATTRIBUTE_UPDATE_APP_VERSION, |
| Integer.toString(updateAppVersion)); |
| int dataAppVersion = status == null |
| ? UNKNOWN_PACKAGE_VERSION : packageVersions.mDataAppVersion; |
| serializer.attribute(namespace, ATTRIBUTE_DATA_APP_VERSION, |
| Integer.toString(dataAppVersion)); |
| serializer.endTag(namespace, TAG_PACKAGE_STATUS); |
| serializer.endDocument(); |
| serializer.flush(); |
| mPackageStatusFile.finishWrite(fos); |
| } catch (IOException e) { |
| if (fos != null) { |
| mPackageStatusFile.failWrite(fos); |
| } |
| throw e; |
| } |
| |
| } |
| |
| /** Only used during tests to force a known table state. */ |
| public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions) { |
| synchronized (this) { |
| try { |
| int optimisticLockId = getCurrentOptimisticLockId(); |
| writePackageStatusWithOptimisticLockCheck(optimisticLockId, optimisticLockId, |
| checkStatus, packageVersions); |
| } catch (IOException | ParseException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| } |
| |
| private static Integer getNullableIntAttribute(XmlPullParser parser, String attributeName) |
| throws ParseException { |
| String attributeValue = parser.getAttributeValue(null, attributeName); |
| try { |
| if (attributeValue == null) { |
| throw new ParseException("Attribute " + attributeName + " missing", 0); |
| } else if (attributeValue.isEmpty()) { |
| return null; |
| } |
| return Integer.parseInt(attributeValue); |
| } catch (NumberFormatException e) { |
| throw new ParseException( |
| "Bad integer for attributeName=" + attributeName + ": " + attributeValue, 0); |
| } |
| } |
| |
| private static int getIntAttribute(XmlPullParser parser, String attributeName) |
| throws ParseException { |
| Integer value = getNullableIntAttribute(parser, attributeName); |
| if (value == null) { |
| throw new ParseException("Missing attribute " + attributeName, 0); |
| } |
| return value; |
| } |
| |
| public void dump(PrintWriter printWriter) { |
| printWriter.println("Package status: " + getPackageStatus()); |
| } |
| } |