Home | History | Annotate | Download | only in timezone
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server.timezone;
     18 
     19 import com.android.internal.annotations.GuardedBy;
     20 import com.android.internal.util.FastXmlSerializer;
     21 
     22 import org.xmlpull.v1.XmlPullParser;
     23 import org.xmlpull.v1.XmlPullParserException;
     24 import org.xmlpull.v1.XmlSerializer;
     25 
     26 import android.util.AtomicFile;
     27 import android.util.Slog;
     28 import android.util.Xml;
     29 
     30 import java.io.File;
     31 import java.io.FileInputStream;
     32 import java.io.FileOutputStream;
     33 import java.io.IOException;
     34 import java.nio.charset.StandardCharsets;
     35 import java.text.ParseException;
     36 import java.io.PrintWriter;
     37 
     38 import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_FAILURE;
     39 import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_SUCCESS;
     40 import static com.android.server.timezone.PackageStatus.CHECK_STARTED;
     41 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
     42 import static org.xmlpull.v1.XmlPullParser.START_TAG;
     43 
     44 /**
     45  * Storage logic for accessing/mutating the Android system's persistent state related to time zone
     46  * update checking. There is expected to be a single instance. All non-private methods are thread
     47  * safe.
     48  */
     49 final class PackageStatusStorage {
     50 
     51     private static final String LOG_TAG = "timezone.PackageStatusStorage";
     52 
     53     private static final String TAG_PACKAGE_STATUS = "PackageStatus";
     54 
     55     /**
     56      * Attribute that stores a monotonically increasing lock ID, used to detect concurrent update
     57      * issues without on-line locks. Incremented on every write.
     58      */
     59     private static final String ATTRIBUTE_OPTIMISTIC_LOCK_ID = "optimisticLockId";
     60 
     61     /**
     62      * Attribute that stores the current "check status" of the time zone update application
     63      * packages.
     64      */
     65     private static final String ATTRIBUTE_CHECK_STATUS = "checkStatus";
     66 
     67     /**
     68      * Attribute that stores the version of the time zone rules update application being checked
     69      * / last checked.
     70      */
     71     private static final String ATTRIBUTE_UPDATE_APP_VERSION = "updateAppPackageVersion";
     72 
     73     /**
     74      * Attribute that stores the version of the time zone rules data application being checked
     75      * / last checked.
     76      */
     77     private static final String ATTRIBUTE_DATA_APP_VERSION = "dataAppPackageVersion";
     78 
     79     private static final int UNKNOWN_PACKAGE_VERSION = -1;
     80 
     81     private final AtomicFile mPackageStatusFile;
     82 
     83     PackageStatusStorage(File storageDir) {
     84         mPackageStatusFile = new AtomicFile(new File(storageDir, "package-status.xml"));
     85         if (!mPackageStatusFile.getBaseFile().exists()) {
     86             try {
     87                 insertInitialPackageStatus();
     88             } catch (IOException e) {
     89                 throw new IllegalStateException(e);
     90             }
     91         }
     92     }
     93 
     94     void deleteFileForTests() {
     95         synchronized(this) {
     96             mPackageStatusFile.delete();
     97         }
     98     }
     99 
    100     /**
    101      * Obtain the current check status of the application packages. Returns {@code null} the first
    102      * time it is called, or after {@link #resetCheckState()}.
    103      */
    104     PackageStatus getPackageStatus() {
    105         synchronized (this) {
    106             try {
    107                 return getPackageStatusLocked();
    108             } catch (ParseException e) {
    109                 // This means that data exists in the file but it was bad.
    110                 Slog.e(LOG_TAG, "Package status invalid, resetting and retrying", e);
    111 
    112                 // Reset the storage so it is in a good state again.
    113                 recoverFromBadData(e);
    114                 try {
    115                     return getPackageStatusLocked();
    116                 } catch (ParseException e2) {
    117                     throw new IllegalStateException("Recovery from bad file failed", e2);
    118                 }
    119             }
    120         }
    121     }
    122 
    123     @GuardedBy("this")
    124     private PackageStatus getPackageStatusLocked() throws ParseException {
    125         try (FileInputStream fis = mPackageStatusFile.openRead()) {
    126             XmlPullParser parser = parseToPackageStatusTag(fis);
    127             Integer checkStatus = getNullableIntAttribute(parser, ATTRIBUTE_CHECK_STATUS);
    128             if (checkStatus == null) {
    129                 return null;
    130             }
    131             int updateAppVersion = getIntAttribute(parser, ATTRIBUTE_UPDATE_APP_VERSION);
    132             int dataAppVersion = getIntAttribute(parser, ATTRIBUTE_DATA_APP_VERSION);
    133             return new PackageStatus(checkStatus,
    134                     new PackageVersions(updateAppVersion, dataAppVersion));
    135         } catch (IOException e) {
    136             ParseException e2 = new ParseException("Error reading package status", 0);
    137             e2.initCause(e);
    138             throw e2;
    139         }
    140     }
    141 
    142     @GuardedBy("this")
    143     private int recoverFromBadData(Exception cause) {
    144         mPackageStatusFile.delete();
    145         try {
    146             return insertInitialPackageStatus();
    147         } catch (IOException e) {
    148             IllegalStateException fatal = new IllegalStateException(e);
    149             fatal.addSuppressed(cause);
    150             throw fatal;
    151         }
    152     }
    153 
    154     /** Insert the initial data, returning the optimistic lock ID */
    155     private int insertInitialPackageStatus() throws IOException {
    156         // Doesn't matter what it is, but we avoid the obvious starting value each time the data
    157         // is reset to ensure that old tokens are unlikely to work.
    158         final int initialOptimisticLockId = (int) System.currentTimeMillis();
    159 
    160         writePackageStatusLocked(null /* status */, initialOptimisticLockId,
    161                 null /* packageVersions */);
    162         return initialOptimisticLockId;
    163     }
    164 
    165     /**
    166      * Generate a new {@link CheckToken} that can be passed to the time zone rules update
    167      * application.
    168      */
    169     CheckToken generateCheckToken(PackageVersions currentInstalledVersions) {
    170         if (currentInstalledVersions == null) {
    171             throw new NullPointerException("currentInstalledVersions == null");
    172         }
    173 
    174         synchronized (this) {
    175             int optimisticLockId;
    176             try {
    177                 optimisticLockId = getCurrentOptimisticLockId();
    178             } catch (ParseException e) {
    179                 Slog.w(LOG_TAG, "Unable to find optimistic lock ID from package status");
    180 
    181                 // Recover.
    182                 optimisticLockId = recoverFromBadData(e);
    183             }
    184 
    185             int newOptimisticLockId = optimisticLockId + 1;
    186             try {
    187                 boolean statusUpdated = writePackageStatusWithOptimisticLockCheck(
    188                         optimisticLockId, newOptimisticLockId, CHECK_STARTED,
    189                         currentInstalledVersions);
    190                 if (!statusUpdated) {
    191                     throw new IllegalStateException("Unable to update status to CHECK_STARTED."
    192                             + " synchronization failure?");
    193                 }
    194                 return new CheckToken(newOptimisticLockId, currentInstalledVersions);
    195             } catch (IOException e) {
    196                 throw new IllegalStateException(e);
    197             }
    198         }
    199     }
    200 
    201     /**
    202      * Reset the current device state to "unknown".
    203      */
    204     void resetCheckState() {
    205         synchronized(this) {
    206             int optimisticLockId;
    207             try {
    208                 optimisticLockId = getCurrentOptimisticLockId();
    209             } catch (ParseException e) {
    210                 Slog.w(LOG_TAG, "resetCheckState: Unable to find optimistic lock ID from package"
    211                         + " status");
    212                 // Attempt to recover the storage state.
    213                 optimisticLockId = recoverFromBadData(e);
    214             }
    215 
    216             int newOptimisticLockId = optimisticLockId + 1;
    217             try {
    218                 if (!writePackageStatusWithOptimisticLockCheck(optimisticLockId,
    219                         newOptimisticLockId, null /* status */, null /* packageVersions */)) {
    220                     throw new IllegalStateException("resetCheckState: Unable to reset package"
    221                             + " status, newOptimisticLockId=" + newOptimisticLockId);
    222                 }
    223             } catch (IOException e) {
    224                 throw new IllegalStateException(e);
    225             }
    226         }
    227     }
    228 
    229     /**
    230      * Update the current device state if possible. Returns true if the update was successful.
    231      * {@code false} indicates the storage has been changed since the {@link CheckToken} was
    232      * generated and the update was discarded.
    233      */
    234     boolean markChecked(CheckToken checkToken, boolean succeeded) {
    235         synchronized (this) {
    236             int optimisticLockId = checkToken.mOptimisticLockId;
    237             int newOptimisticLockId = optimisticLockId + 1;
    238             int status = succeeded ? CHECK_COMPLETED_SUCCESS : CHECK_COMPLETED_FAILURE;
    239             try {
    240                 return writePackageStatusWithOptimisticLockCheck(optimisticLockId,
    241                         newOptimisticLockId, status, checkToken.mPackageVersions);
    242             } catch (IOException e) {
    243                 throw new IllegalStateException(e);
    244             }
    245         }
    246     }
    247 
    248     @GuardedBy("this")
    249     private int getCurrentOptimisticLockId() throws ParseException {
    250         try (FileInputStream fis = mPackageStatusFile.openRead()) {
    251             XmlPullParser parser = parseToPackageStatusTag(fis);
    252             return getIntAttribute(parser, ATTRIBUTE_OPTIMISTIC_LOCK_ID);
    253         } catch (IOException e) {
    254             ParseException e2 = new ParseException("Unable to read file", 0);
    255             e2.initCause(e);
    256             throw e2;
    257         }
    258     }
    259 
    260     /** Returns a parser or throws ParseException, never returns null. */
    261     private static XmlPullParser parseToPackageStatusTag(FileInputStream fis)
    262             throws ParseException {
    263         try {
    264             XmlPullParser parser = Xml.newPullParser();
    265             parser.setInput(fis, StandardCharsets.UTF_8.name());
    266             int type;
    267             while ((type = parser.next()) != END_DOCUMENT) {
    268                 final String tag = parser.getName();
    269                 if (type == START_TAG && TAG_PACKAGE_STATUS.equals(tag)) {
    270                     return parser;
    271                 }
    272             }
    273             throw new ParseException("Unable to find " + TAG_PACKAGE_STATUS + " tag", 0);
    274         } catch (XmlPullParserException e) {
    275             throw new IllegalStateException("Unable to configure parser", e);
    276         } catch (IOException e) {
    277             ParseException e2 = new ParseException("Error reading XML", 0);
    278             e.initCause(e);
    279             throw e2;
    280         }
    281     }
    282 
    283     @GuardedBy("this")
    284     private boolean writePackageStatusWithOptimisticLockCheck(int optimisticLockId,
    285             int newOptimisticLockId, Integer status, PackageVersions packageVersions)
    286             throws IOException {
    287 
    288         int currentOptimisticLockId;
    289         try {
    290             currentOptimisticLockId = getCurrentOptimisticLockId();
    291             if (currentOptimisticLockId != optimisticLockId) {
    292                 return false;
    293             }
    294         } catch (ParseException e) {
    295             recoverFromBadData(e);
    296             return false;
    297         }
    298 
    299         writePackageStatusLocked(status, newOptimisticLockId, packageVersions);
    300         return true;
    301     }
    302 
    303     @GuardedBy("this")
    304     private void writePackageStatusLocked(Integer status, int optimisticLockId,
    305             PackageVersions packageVersions) throws IOException {
    306         if ((status == null) != (packageVersions == null)) {
    307             throw new IllegalArgumentException(
    308                     "Provide both status and packageVersions, or neither.");
    309         }
    310 
    311         FileOutputStream fos = null;
    312         try {
    313             fos = mPackageStatusFile.startWrite();
    314             XmlSerializer serializer = new FastXmlSerializer();
    315             serializer.setOutput(fos, StandardCharsets.UTF_8.name());
    316             serializer.startDocument(null /* encoding */, true /* standalone */);
    317             final String namespace = null;
    318             serializer.startTag(namespace, TAG_PACKAGE_STATUS);
    319             String statusAttributeValue = status == null ? "" : Integer.toString(status);
    320             serializer.attribute(namespace, ATTRIBUTE_CHECK_STATUS, statusAttributeValue);
    321             serializer.attribute(namespace, ATTRIBUTE_OPTIMISTIC_LOCK_ID,
    322                     Integer.toString(optimisticLockId));
    323             int updateAppVersion = status == null
    324                     ? UNKNOWN_PACKAGE_VERSION : packageVersions.mUpdateAppVersion;
    325             serializer.attribute(namespace, ATTRIBUTE_UPDATE_APP_VERSION,
    326                     Integer.toString(updateAppVersion));
    327             int dataAppVersion = status == null
    328                     ? UNKNOWN_PACKAGE_VERSION : packageVersions.mDataAppVersion;
    329             serializer.attribute(namespace, ATTRIBUTE_DATA_APP_VERSION,
    330                     Integer.toString(dataAppVersion));
    331             serializer.endTag(namespace, TAG_PACKAGE_STATUS);
    332             serializer.endDocument();
    333             serializer.flush();
    334             mPackageStatusFile.finishWrite(fos);
    335         } catch (IOException e) {
    336             if (fos != null) {
    337                 mPackageStatusFile.failWrite(fos);
    338             }
    339             throw e;
    340         }
    341 
    342     }
    343 
    344     /** Only used during tests to force a known table state. */
    345     public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions) {
    346         synchronized (this) {
    347             try {
    348                 int optimisticLockId = getCurrentOptimisticLockId();
    349                 writePackageStatusWithOptimisticLockCheck(optimisticLockId, optimisticLockId,
    350                         checkStatus, packageVersions);
    351             } catch (IOException | ParseException e) {
    352                 throw new IllegalStateException(e);
    353             }
    354         }
    355     }
    356 
    357     private static Integer getNullableIntAttribute(XmlPullParser parser, String attributeName)
    358             throws ParseException {
    359         String attributeValue = parser.getAttributeValue(null, attributeName);
    360         try {
    361             if (attributeValue == null) {
    362                 throw new ParseException("Attribute " + attributeName + " missing", 0);
    363             } else if (attributeValue.isEmpty()) {
    364                 return null;
    365             }
    366             return Integer.parseInt(attributeValue);
    367         } catch (NumberFormatException e) {
    368             throw new ParseException(
    369                     "Bad integer for attributeName=" + attributeName + ": " + attributeValue, 0);
    370         }
    371     }
    372 
    373     private static int getIntAttribute(XmlPullParser parser, String attributeName)
    374             throws ParseException {
    375         Integer value = getNullableIntAttribute(parser, attributeName);
    376         if (value == null) {
    377             throw new ParseException("Missing attribute " + attributeName, 0);
    378         }
    379         return value;
    380     }
    381 
    382     public void dump(PrintWriter printWriter) {
    383         printWriter.println("Package status: " + getPackageStatus());
    384     }
    385 }
    386