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