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 PackageStatusStorage(File storageDir)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 */ initialize()92 void initialize() throws IOException { 93 if (!mPackageStatusFile.getBaseFile().exists()) { 94 insertInitialPackageStatus(); 95 } 96 } 97 deleteFileForTests()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 */ getPackageStatus()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") getPackageStatusLocked()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") recoverFromBadData(Exception cause)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 */ insertInitialPackageStatus()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 */ generateCheckToken(PackageVersions currentInstalledVersions)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 */ resetCheckState()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 */ markChecked(CheckToken checkToken, boolean succeeded)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") getCurrentOptimisticLockId()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. */ parseToPackageStatusTag(FileInputStream fis)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") writePackageStatusWithOptimisticLockCheck(int optimisticLockId, int newOptimisticLockId, Integer status, PackageVersions packageVersions)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") writePackageStatusLocked(Integer status, int optimisticLockId, PackageVersions packageVersions)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. */ 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(XmlPullParser parser, String attributeName)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 getIntAttribute(XmlPullParser parser, String attributeName)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 dump(PrintWriter printWriter)386 public void dump(PrintWriter printWriter) { 387 printWriter.println("Package status: " + getPackageStatus()); 388 } 389 } 390