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 long 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"), "timezone-status");
     85     }
     86 
     87     /**
     88      * Initialize any storage, as needed.
     89      *
     90      * @throws IOException if the storage could not be initialized
     91      */
     92     void initialize() throws IOException {
     93         if (!mPackageStatusFile.getBaseFile().exists()) {
     94             insertInitialPackageStatus();
     95         }
     96     }
     97 
     98     void deleteFileForTests() {
     99         synchronized(this) {
    100             mPackageStatusFile.delete();
    101         }
    102     }
    103 
    104     /**
    105      * Obtain the current check status of the application packages. Returns {@code null} the first
    106      * time it is called, or after {@link #resetCheckState()}.
    107      */
    108     PackageStatus getPackageStatus() {
    109         synchronized (this) {
    110             try {
    111                 return getPackageStatusLocked();
    112             } catch (ParseException e) {
    113                 // This means that data exists in the file but it was bad.
    114                 Slog.e(LOG_TAG, "Package status invalid, resetting and retrying", e);
    115 
    116                 // Reset the storage so it is in a good state again.
    117                 recoverFromBadData(e);
    118                 try {
    119                     return getPackageStatusLocked();
    120                 } catch (ParseException e2) {
    121                     throw new IllegalStateException("Recovery from bad file failed", e2);
    122                 }
    123             }
    124         }
    125     }
    126 
    127     @GuardedBy("this")
    128     private PackageStatus getPackageStatusLocked() throws ParseException {
    129         try (FileInputStream fis = mPackageStatusFile.openRead()) {
    130             XmlPullParser parser = parseToPackageStatusTag(fis);
    131             Integer checkStatus = getNullableIntAttribute(parser, ATTRIBUTE_CHECK_STATUS);
    132             if (checkStatus == null) {
    133                 return null;
    134             }
    135             int updateAppVersion = getIntAttribute(parser, ATTRIBUTE_UPDATE_APP_VERSION);
    136             int dataAppVersion = getIntAttribute(parser, ATTRIBUTE_DATA_APP_VERSION);
    137             return new PackageStatus(checkStatus,
    138                     new PackageVersions(updateAppVersion, dataAppVersion));
    139         } catch (IOException e) {
    140             ParseException e2 = new ParseException("Error reading package status", 0);
    141             e2.initCause(e);
    142             throw e2;
    143         }
    144     }
    145 
    146     @GuardedBy("this")
    147     private int recoverFromBadData(Exception cause) {
    148         mPackageStatusFile.delete();
    149         try {
    150             return insertInitialPackageStatus();
    151         } catch (IOException e) {
    152             IllegalStateException fatal = new IllegalStateException(e);
    153             fatal.addSuppressed(cause);
    154             throw fatal;
    155         }
    156     }
    157 
    158     /** Insert the initial data, returning the optimistic lock ID */
    159     private int insertInitialPackageStatus() throws IOException {
    160         // Doesn't matter what it is, but we avoid the obvious starting value each time the data
    161         // is reset to ensure that old tokens are unlikely to work.
    162         final int initialOptimisticLockId = (int) System.currentTimeMillis();
    163 
    164         writePackageStatusLocked(null /* status */, initialOptimisticLockId,
    165                 null /* packageVersions */);
    166         return initialOptimisticLockId;
    167     }
    168 
    169     /**
    170      * Generate a new {@link CheckToken} that can be passed to the time zone rules update
    171      * application.
    172      */
    173     CheckToken generateCheckToken(PackageVersions currentInstalledVersions) {
    174         if (currentInstalledVersions == null) {
    175             throw new NullPointerException("currentInstalledVersions == null");
    176         }
    177 
    178         synchronized (this) {
    179             int optimisticLockId;
    180             try {
    181                 optimisticLockId = getCurrentOptimisticLockId();
    182             } catch (ParseException e) {
    183                 Slog.w(LOG_TAG, "Unable to find optimistic lock ID from package status");
    184 
    185                 // Recover.
    186                 optimisticLockId = recoverFromBadData(e);
    187             }
    188 
    189             int newOptimisticLockId = optimisticLockId + 1;
    190             try {
    191                 boolean statusUpdated = writePackageStatusWithOptimisticLockCheck(
    192                         optimisticLockId, newOptimisticLockId, CHECK_STARTED,
    193                         currentInstalledVersions);
    194                 if (!statusUpdated) {
    195                     throw new IllegalStateException("Unable to update status to CHECK_STARTED."
    196                             + " synchronization failure?");
    197                 }
    198                 return new CheckToken(newOptimisticLockId, currentInstalledVersions);
    199             } catch (IOException e) {
    200                 throw new IllegalStateException(e);
    201             }
    202         }
    203     }
    204 
    205     /**
    206      * Reset the current device state to "unknown".
    207      */
    208     void resetCheckState() {
    209         synchronized(this) {
    210             int optimisticLockId;
    211             try {
    212                 optimisticLockId = getCurrentOptimisticLockId();
    213             } catch (ParseException e) {
    214                 Slog.w(LOG_TAG, "resetCheckState: Unable to find optimistic lock ID from package"
    215                         + " status");
    216                 // Attempt to recover the storage state.
    217                 optimisticLockId = recoverFromBadData(e);
    218             }
    219 
    220             int newOptimisticLockId = optimisticLockId + 1;
    221             try {
    222                 if (!writePackageStatusWithOptimisticLockCheck(optimisticLockId,
    223                         newOptimisticLockId, null /* status */, null /* packageVersions */)) {
    224                     throw new IllegalStateException("resetCheckState: Unable to reset package"
    225                             + " status, newOptimisticLockId=" + newOptimisticLockId);
    226                 }
    227             } catch (IOException e) {
    228                 throw new IllegalStateException(e);
    229             }
    230         }
    231     }
    232 
    233     /**
    234      * Update the current device state if possible. Returns true if the update was successful.
    235      * {@code false} indicates the storage has been changed since the {@link CheckToken} was
    236      * generated and the update was discarded.
    237      */
    238     boolean markChecked(CheckToken checkToken, boolean succeeded) {
    239         synchronized (this) {
    240             int optimisticLockId = checkToken.mOptimisticLockId;
    241             int newOptimisticLockId = optimisticLockId + 1;
    242             int status = succeeded ? CHECK_COMPLETED_SUCCESS : CHECK_COMPLETED_FAILURE;
    243             try {
    244                 return writePackageStatusWithOptimisticLockCheck(optimisticLockId,
    245                         newOptimisticLockId, status, checkToken.mPackageVersions);
    246             } catch (IOException e) {
    247                 throw new IllegalStateException(e);
    248             }
    249         }
    250     }
    251 
    252     @GuardedBy("this")
    253     private int getCurrentOptimisticLockId() throws ParseException {
    254         try (FileInputStream fis = mPackageStatusFile.openRead()) {
    255             XmlPullParser parser = parseToPackageStatusTag(fis);
    256             return getIntAttribute(parser, ATTRIBUTE_OPTIMISTIC_LOCK_ID);
    257         } catch (IOException e) {
    258             ParseException e2 = new ParseException("Unable to read file", 0);
    259             e2.initCause(e);
    260             throw e2;
    261         }
    262     }
    263 
    264     /** Returns a parser or throws ParseException, never returns null. */
    265     private static XmlPullParser parseToPackageStatusTag(FileInputStream fis)
    266             throws ParseException {
    267         try {
    268             XmlPullParser parser = Xml.newPullParser();
    269             parser.setInput(fis, StandardCharsets.UTF_8.name());
    270             int type;
    271             while ((type = parser.next()) != END_DOCUMENT) {
    272                 final String tag = parser.getName();
    273                 if (type == START_TAG && TAG_PACKAGE_STATUS.equals(tag)) {
    274                     return parser;
    275                 }
    276             }
    277             throw new ParseException("Unable to find " + TAG_PACKAGE_STATUS + " tag", 0);
    278         } catch (XmlPullParserException e) {
    279             throw new IllegalStateException("Unable to configure parser", e);
    280         } catch (IOException e) {
    281             ParseException e2 = new ParseException("Error reading XML", 0);
    282             e.initCause(e);
    283             throw e2;
    284         }
    285     }
    286 
    287     @GuardedBy("this")
    288     private boolean writePackageStatusWithOptimisticLockCheck(int optimisticLockId,
    289             int newOptimisticLockId, Integer status, PackageVersions packageVersions)
    290             throws IOException {
    291 
    292         int currentOptimisticLockId;
    293         try {
    294             currentOptimisticLockId = getCurrentOptimisticLockId();
    295             if (currentOptimisticLockId != optimisticLockId) {
    296                 return false;
    297             }
    298         } catch (ParseException e) {
    299             recoverFromBadData(e);
    300             return false;
    301         }
    302 
    303         writePackageStatusLocked(status, newOptimisticLockId, packageVersions);
    304         return true;
    305     }
    306 
    307     @GuardedBy("this")
    308     private void writePackageStatusLocked(Integer status, int optimisticLockId,
    309             PackageVersions packageVersions) throws IOException {
    310         if ((status == null) != (packageVersions == null)) {
    311             throw new IllegalArgumentException(
    312                     "Provide both status and packageVersions, or neither.");
    313         }
    314 
    315         FileOutputStream fos = null;
    316         try {
    317             fos = mPackageStatusFile.startWrite();
    318             XmlSerializer serializer = new FastXmlSerializer();
    319             serializer.setOutput(fos, StandardCharsets.UTF_8.name());
    320             serializer.startDocument(null /* encoding */, true /* standalone */);
    321             final String namespace = null;
    322             serializer.startTag(namespace, TAG_PACKAGE_STATUS);
    323             String statusAttributeValue = status == null ? "" : Integer.toString(status);
    324             serializer.attribute(namespace, ATTRIBUTE_CHECK_STATUS, statusAttributeValue);
    325             serializer.attribute(namespace, ATTRIBUTE_OPTIMISTIC_LOCK_ID,
    326                     Integer.toString(optimisticLockId));
    327             long updateAppVersion = status == null
    328                     ? UNKNOWN_PACKAGE_VERSION : packageVersions.mUpdateAppVersion;
    329             serializer.attribute(namespace, ATTRIBUTE_UPDATE_APP_VERSION,
    330                     Long.toString(updateAppVersion));
    331             long dataAppVersion = status == null
    332                     ? UNKNOWN_PACKAGE_VERSION : packageVersions.mDataAppVersion;
    333             serializer.attribute(namespace, ATTRIBUTE_DATA_APP_VERSION,
    334                     Long.toString(dataAppVersion));
    335             serializer.endTag(namespace, TAG_PACKAGE_STATUS);
    336             serializer.endDocument();
    337             serializer.flush();
    338             mPackageStatusFile.finishWrite(fos);
    339         } catch (IOException e) {
    340             if (fos != null) {
    341                 mPackageStatusFile.failWrite(fos);
    342             }
    343             throw e;
    344         }
    345 
    346     }
    347 
    348     /** Only used during tests to force a known table state. */
    349     public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions)
    350             throws IOException {
    351         synchronized (this) {
    352             try {
    353                 final int initialOptimisticLockId = (int) System.currentTimeMillis();
    354                 writePackageStatusLocked(checkStatus, initialOptimisticLockId, packageVersions);
    355             } catch (IOException e) {
    356                 throw new IllegalStateException(e);
    357             }
    358         }
    359     }
    360 
    361     private static Integer getNullableIntAttribute(XmlPullParser parser, String attributeName)
    362             throws ParseException {
    363         String attributeValue = parser.getAttributeValue(null, attributeName);
    364         try {
    365             if (attributeValue == null) {
    366                 throw new ParseException("Attribute " + attributeName + " missing", 0);
    367             } else if (attributeValue.isEmpty()) {
    368                 return null;
    369             }
    370             return Integer.parseInt(attributeValue);
    371         } catch (NumberFormatException e) {
    372             throw new ParseException(
    373                     "Bad integer for attributeName=" + attributeName + ": " + attributeValue, 0);
    374         }
    375     }
    376 
    377     private static int getIntAttribute(XmlPullParser parser, String attributeName)
    378             throws ParseException {
    379         Integer value = getNullableIntAttribute(parser, attributeName);
    380         if (value == null) {
    381             throw new ParseException("Missing attribute " + attributeName, 0);
    382         }
    383         return value;
    384     }
    385 
    386     public void dump(PrintWriter printWriter) {
    387         printWriter.println("Package status: " + getPackageStatus());
    388     }
    389 }
    390