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.TypedXmlPullParser; 29 import android.util.TypedXmlSerializer; 30 import android.util.Xml; 31 32 import java.io.File; 33 import java.io.FileInputStream; 34 import java.io.FileOutputStream; 35 import java.io.IOException; 36 import java.nio.charset.StandardCharsets; 37 import java.text.ParseException; 38 import java.io.PrintWriter; 39 40 import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_FAILURE; 41 import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_SUCCESS; 42 import static com.android.server.timezone.PackageStatus.CHECK_STARTED; 43 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; 44 import static org.xmlpull.v1.XmlPullParser.START_TAG; 45 46 /** 47 * Storage logic for accessing/mutating the Android system's persistent state related to time zone 48 * update checking. There is expected to be a single instance. All non-private methods are thread 49 * safe. 50 */ 51 final class PackageStatusStorage { 52 53 private static final String LOG_TAG = "timezone.PackageStatusStorage"; 54 55 private static final String TAG_PACKAGE_STATUS = "PackageStatus"; 56 57 /** 58 * Attribute that stores a monotonically increasing lock ID, used to detect concurrent update 59 * issues without on-line locks. Incremented on every write. 60 */ 61 private static final String ATTRIBUTE_OPTIMISTIC_LOCK_ID = "optimisticLockId"; 62 63 /** 64 * Attribute that stores the current "check status" of the time zone update application 65 * packages. 66 */ 67 private static final String ATTRIBUTE_CHECK_STATUS = "checkStatus"; 68 69 /** 70 * Attribute that stores the version of the time zone rules update application being checked 71 * / last checked. 72 */ 73 private static final String ATTRIBUTE_UPDATE_APP_VERSION = "updateAppPackageVersion"; 74 75 /** 76 * Attribute that stores the version of the time zone rules data application being checked 77 * / last checked. 78 */ 79 private static final String ATTRIBUTE_DATA_APP_VERSION = "dataAppPackageVersion"; 80 81 private static final long UNKNOWN_PACKAGE_VERSION = -1; 82 83 private final AtomicFile mPackageStatusFile; 84 PackageStatusStorage(File storageDir)85 PackageStatusStorage(File storageDir) { 86 mPackageStatusFile = new AtomicFile(new File(storageDir, "package-status.xml"), "timezone-status"); 87 } 88 89 /** 90 * Initialize any storage, as needed. 91 * 92 * @throws IOException if the storage could not be initialized 93 */ initialize()94 void initialize() throws IOException { 95 if (!mPackageStatusFile.getBaseFile().exists()) { 96 insertInitialPackageStatus(); 97 } 98 } 99 deleteFileForTests()100 void deleteFileForTests() { 101 synchronized(this) { 102 mPackageStatusFile.delete(); 103 } 104 } 105 106 /** 107 * Obtain the current check status of the application packages. Returns {@code null} the first 108 * time it is called, or after {@link #resetCheckState()}. 109 */ getPackageStatus()110 PackageStatus getPackageStatus() { 111 synchronized (this) { 112 try { 113 return getPackageStatusLocked(); 114 } catch (ParseException e) { 115 // This means that data exists in the file but it was bad. 116 Slog.e(LOG_TAG, "Package status invalid, resetting and retrying", e); 117 118 // Reset the storage so it is in a good state again. 119 recoverFromBadData(e); 120 try { 121 return getPackageStatusLocked(); 122 } catch (ParseException e2) { 123 throw new IllegalStateException("Recovery from bad file failed", e2); 124 } 125 } 126 } 127 } 128 129 @GuardedBy("this") getPackageStatusLocked()130 private PackageStatus getPackageStatusLocked() throws ParseException { 131 try (FileInputStream fis = mPackageStatusFile.openRead()) { 132 TypedXmlPullParser parser = parseToPackageStatusTag(fis); 133 Integer checkStatus = getNullableIntAttribute(parser, ATTRIBUTE_CHECK_STATUS); 134 if (checkStatus == null) { 135 return null; 136 } 137 int updateAppVersion = getIntAttribute(parser, ATTRIBUTE_UPDATE_APP_VERSION); 138 int dataAppVersion = getIntAttribute(parser, ATTRIBUTE_DATA_APP_VERSION); 139 return new PackageStatus(checkStatus, 140 new PackageVersions(updateAppVersion, dataAppVersion)); 141 } catch (IOException e) { 142 ParseException e2 = new ParseException("Error reading package status", 0); 143 e2.initCause(e); 144 throw e2; 145 } 146 } 147 148 @GuardedBy("this") recoverFromBadData(Exception cause)149 private int recoverFromBadData(Exception cause) { 150 mPackageStatusFile.delete(); 151 try { 152 return insertInitialPackageStatus(); 153 } catch (IOException e) { 154 IllegalStateException fatal = new IllegalStateException(e); 155 fatal.addSuppressed(cause); 156 throw fatal; 157 } 158 } 159 160 /** Insert the initial data, returning the optimistic lock ID */ insertInitialPackageStatus()161 private int insertInitialPackageStatus() throws IOException { 162 // Doesn't matter what it is, but we avoid the obvious starting value each time the data 163 // is reset to ensure that old tokens are unlikely to work. 164 final int initialOptimisticLockId = (int) System.currentTimeMillis(); 165 166 writePackageStatusLocked(null /* status */, initialOptimisticLockId, 167 null /* packageVersions */); 168 return initialOptimisticLockId; 169 } 170 171 /** 172 * Generate a new {@link CheckToken} that can be passed to the time zone rules update 173 * application. 174 */ generateCheckToken(PackageVersions currentInstalledVersions)175 CheckToken generateCheckToken(PackageVersions currentInstalledVersions) { 176 if (currentInstalledVersions == null) { 177 throw new NullPointerException("currentInstalledVersions == null"); 178 } 179 180 synchronized (this) { 181 int optimisticLockId; 182 try { 183 optimisticLockId = getCurrentOptimisticLockId(); 184 } catch (ParseException e) { 185 Slog.w(LOG_TAG, "Unable to find optimistic lock ID from package status"); 186 187 // Recover. 188 optimisticLockId = recoverFromBadData(e); 189 } 190 191 int newOptimisticLockId = optimisticLockId + 1; 192 try { 193 boolean statusUpdated = writePackageStatusWithOptimisticLockCheck( 194 optimisticLockId, newOptimisticLockId, CHECK_STARTED, 195 currentInstalledVersions); 196 if (!statusUpdated) { 197 throw new IllegalStateException("Unable to update status to CHECK_STARTED." 198 + " synchronization failure?"); 199 } 200 return new CheckToken(newOptimisticLockId, currentInstalledVersions); 201 } catch (IOException e) { 202 throw new IllegalStateException(e); 203 } 204 } 205 } 206 207 /** 208 * Reset the current device state to "unknown". 209 */ resetCheckState()210 void resetCheckState() { 211 synchronized(this) { 212 int optimisticLockId; 213 try { 214 optimisticLockId = getCurrentOptimisticLockId(); 215 } catch (ParseException e) { 216 Slog.w(LOG_TAG, "resetCheckState: Unable to find optimistic lock ID from package" 217 + " status"); 218 // Attempt to recover the storage state. 219 optimisticLockId = recoverFromBadData(e); 220 } 221 222 int newOptimisticLockId = optimisticLockId + 1; 223 try { 224 if (!writePackageStatusWithOptimisticLockCheck(optimisticLockId, 225 newOptimisticLockId, null /* status */, null /* packageVersions */)) { 226 throw new IllegalStateException("resetCheckState: Unable to reset package" 227 + " status, newOptimisticLockId=" + newOptimisticLockId); 228 } 229 } catch (IOException e) { 230 throw new IllegalStateException(e); 231 } 232 } 233 } 234 235 /** 236 * Update the current device state if possible. Returns true if the update was successful. 237 * {@code false} indicates the storage has been changed since the {@link CheckToken} was 238 * generated and the update was discarded. 239 */ markChecked(CheckToken checkToken, boolean succeeded)240 boolean markChecked(CheckToken checkToken, boolean succeeded) { 241 synchronized (this) { 242 int optimisticLockId = checkToken.mOptimisticLockId; 243 int newOptimisticLockId = optimisticLockId + 1; 244 int status = succeeded ? CHECK_COMPLETED_SUCCESS : CHECK_COMPLETED_FAILURE; 245 try { 246 return writePackageStatusWithOptimisticLockCheck(optimisticLockId, 247 newOptimisticLockId, status, checkToken.mPackageVersions); 248 } catch (IOException e) { 249 throw new IllegalStateException(e); 250 } 251 } 252 } 253 254 @GuardedBy("this") getCurrentOptimisticLockId()255 private int getCurrentOptimisticLockId() throws ParseException { 256 try (FileInputStream fis = mPackageStatusFile.openRead()) { 257 TypedXmlPullParser parser = parseToPackageStatusTag(fis); 258 return getIntAttribute(parser, ATTRIBUTE_OPTIMISTIC_LOCK_ID); 259 } catch (IOException e) { 260 ParseException e2 = new ParseException("Unable to read file", 0); 261 e2.initCause(e); 262 throw e2; 263 } 264 } 265 266 /** Returns a parser or throws ParseException, never returns null. */ parseToPackageStatusTag(FileInputStream fis)267 private static TypedXmlPullParser parseToPackageStatusTag(FileInputStream fis) 268 throws ParseException { 269 try { 270 TypedXmlPullParser parser = Xml.resolvePullParser(fis); 271 int type; 272 while ((type = parser.next()) != END_DOCUMENT) { 273 final String tag = parser.getName(); 274 if (type == START_TAG && TAG_PACKAGE_STATUS.equals(tag)) { 275 return parser; 276 } 277 } 278 throw new ParseException("Unable to find " + TAG_PACKAGE_STATUS + " tag", 0); 279 } catch (XmlPullParserException e) { 280 throw new IllegalStateException("Unable to configure parser", e); 281 } catch (IOException e) { 282 ParseException e2 = new ParseException("Error reading XML", 0); 283 e.initCause(e); 284 throw e2; 285 } 286 } 287 288 @GuardedBy("this") writePackageStatusWithOptimisticLockCheck(int optimisticLockId, int newOptimisticLockId, Integer status, PackageVersions packageVersions)289 private boolean writePackageStatusWithOptimisticLockCheck(int optimisticLockId, 290 int newOptimisticLockId, Integer status, PackageVersions packageVersions) 291 throws IOException { 292 293 int currentOptimisticLockId; 294 try { 295 currentOptimisticLockId = getCurrentOptimisticLockId(); 296 if (currentOptimisticLockId != optimisticLockId) { 297 return false; 298 } 299 } catch (ParseException e) { 300 recoverFromBadData(e); 301 return false; 302 } 303 304 writePackageStatusLocked(status, newOptimisticLockId, packageVersions); 305 return true; 306 } 307 308 @GuardedBy("this") writePackageStatusLocked(Integer status, int optimisticLockId, PackageVersions packageVersions)309 private void writePackageStatusLocked(Integer status, int optimisticLockId, 310 PackageVersions packageVersions) throws IOException { 311 if ((status == null) != (packageVersions == null)) { 312 throw new IllegalArgumentException( 313 "Provide both status and packageVersions, or neither."); 314 } 315 316 FileOutputStream fos = null; 317 try { 318 fos = mPackageStatusFile.startWrite(); 319 TypedXmlSerializer serializer = Xml.resolveSerializer(fos); 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. */ forceCheckStateForTests(int checkStatus, PackageVersions packageVersions)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 getNullableIntAttribute(TypedXmlPullParser parser, String attributeName)361 private static Integer getNullableIntAttribute(TypedXmlPullParser 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 getIntAttribute(TypedXmlPullParser parser, String attributeName)377 private static int getIntAttribute(TypedXmlPullParser 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 dump(PrintWriter printWriter)386 public void dump(PrintWriter printWriter) { 387 printWriter.println("Package status: " + getPackageStatus()); 388 } 389 } 390