blob: cac7f7b811bff935ea787a726af61bb84eafd211 [file] [log] [blame]
Neil Fuller68f66662017-03-16 18:32:21 +00001/*
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
17package com.android.server.timezone;
18
Neil Fuller316fb6072017-07-11 11:34:25 +010019import com.android.internal.annotations.GuardedBy;
Neil Fuller5f6750f2017-05-17 04:43:12 +010020import com.android.internal.util.FastXmlSerializer;
21
22import org.xmlpull.v1.XmlPullParser;
23import org.xmlpull.v1.XmlPullParserException;
24import org.xmlpull.v1.XmlSerializer;
25
26import android.util.AtomicFile;
Neil Fuller68f66662017-03-16 18:32:21 +000027import android.util.Slog;
Neil Fuller5f6750f2017-05-17 04:43:12 +010028import android.util.Xml;
Neil Fuller68f66662017-03-16 18:32:21 +000029
30import java.io.File;
Neil Fuller5f6750f2017-05-17 04:43:12 +010031import java.io.FileInputStream;
32import java.io.FileOutputStream;
33import java.io.IOException;
34import java.nio.charset.StandardCharsets;
35import java.text.ParseException;
Neil Fuller87b11282017-06-23 16:43:45 +010036import java.io.PrintWriter;
Neil Fuller68f66662017-03-16 18:32:21 +000037
38import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_FAILURE;
39import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_SUCCESS;
40import static com.android.server.timezone.PackageStatus.CHECK_STARTED;
Neil Fuller5f6750f2017-05-17 04:43:12 +010041import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
42import static org.xmlpull.v1.XmlPullParser.START_TAG;
Neil Fuller68f66662017-03-16 18:32:21 +000043
44/**
45 * Storage logic for accessing/mutating the Android system's persistent state related to time zone
Neil Fuller5f6750f2017-05-17 04:43:12 +010046 * update checking. There is expected to be a single instance. All non-private methods are thread
47 * safe.
Neil Fuller68f66662017-03-16 18:32:21 +000048 */
49final class PackageStatusStorage {
50
Neil Fuller5f6750f2017-05-17 04:43:12 +010051 private static final String LOG_TAG = "timezone.PackageStatusStorage";
Neil Fuller68f66662017-03-16 18:32:21 +000052
Neil Fuller5f6750f2017-05-17 04:43:12 +010053 private static final String TAG_PACKAGE_STATUS = "PackageStatus";
Neil Fuller68f66662017-03-16 18:32:21 +000054
55 /**
Neil Fuller5f6750f2017-05-17 04:43:12 +010056 * Attribute that stores a monotonically increasing lock ID, used to detect concurrent update
Neil Fuller68f66662017-03-16 18:32:21 +000057 * issues without on-line locks. Incremented on every write.
58 */
Neil Fuller5f6750f2017-05-17 04:43:12 +010059 private static final String ATTRIBUTE_OPTIMISTIC_LOCK_ID = "optimisticLockId";
Neil Fuller68f66662017-03-16 18:32:21 +000060
61 /**
Neil Fuller5f6750f2017-05-17 04:43:12 +010062 * Attribute that stores the current "check status" of the time zone update application
63 * packages.
Neil Fuller68f66662017-03-16 18:32:21 +000064 */
Neil Fuller5f6750f2017-05-17 04:43:12 +010065 private static final String ATTRIBUTE_CHECK_STATUS = "checkStatus";
Neil Fuller68f66662017-03-16 18:32:21 +000066
67 /**
Neil Fuller5f6750f2017-05-17 04:43:12 +010068 * Attribute that stores the version of the time zone rules update application being checked
69 * / last checked.
Neil Fuller68f66662017-03-16 18:32:21 +000070 */
Neil Fuller5f6750f2017-05-17 04:43:12 +010071 private static final String ATTRIBUTE_UPDATE_APP_VERSION = "updateAppPackageVersion";
Neil Fuller68f66662017-03-16 18:32:21 +000072
73 /**
Neil Fuller5f6750f2017-05-17 04:43:12 +010074 * Attribute that stores the version of the time zone rules data application being checked
75 * / last checked.
Neil Fuller68f66662017-03-16 18:32:21 +000076 */
Neil Fuller5f6750f2017-05-17 04:43:12 +010077 private static final String ATTRIBUTE_DATA_APP_VERSION = "dataAppPackageVersion";
Neil Fuller68f66662017-03-16 18:32:21 +000078
79 private static final int UNKNOWN_PACKAGE_VERSION = -1;
80
Neil Fuller5f6750f2017-05-17 04:43:12 +010081 private final AtomicFile mPackageStatusFile;
Neil Fuller68f66662017-03-16 18:32:21 +000082
Neil Fuller5f6750f2017-05-17 04:43:12 +010083 PackageStatusStorage(File storageDir) {
Neil Fuller316fb6072017-07-11 11:34:25 +010084 mPackageStatusFile = new AtomicFile(new File(storageDir, "package-status.xml"));
Neil Fuller5f6750f2017-05-17 04:43:12 +010085 if (!mPackageStatusFile.getBaseFile().exists()) {
86 try {
87 insertInitialPackageStatus();
88 } catch (IOException e) {
89 throw new IllegalStateException(e);
90 }
91 }
Neil Fuller68f66662017-03-16 18:32:21 +000092 }
93
Neil Fuller5f6750f2017-05-17 04:43:12 +010094 void deleteFileForTests() {
95 synchronized(this) {
96 mPackageStatusFile.delete();
97 }
Neil Fuller68f66662017-03-16 18:32:21 +000098 }
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 {
Neil Fuller316fb6072017-07-11 11:34:25 +0100107 return getPackageStatusLocked();
Neil Fuller5f6750f2017-05-17 04:43:12 +0100108 } 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);
Neil Fuller68f66662017-03-16 18:32:21 +0000111
112 // Reset the storage so it is in a good state again.
Neil Fuller5f6750f2017-05-17 04:43:12 +0100113 recoverFromBadData(e);
114 try {
Neil Fuller316fb6072017-07-11 11:34:25 +0100115 return getPackageStatusLocked();
Neil Fuller5f6750f2017-05-17 04:43:12 +0100116 } catch (ParseException e2) {
117 throw new IllegalStateException("Recovery from bad file failed", e2);
118 }
Neil Fuller68f66662017-03-16 18:32:21 +0000119 }
120 }
121 }
122
Neil Fuller316fb6072017-07-11 11:34:25 +0100123 @GuardedBy("this")
124 private PackageStatus getPackageStatusLocked() throws ParseException {
Neil Fuller5f6750f2017-05-17 04:43:12 +0100125 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;
Neil Fuller68f66662017-03-16 18:32:21 +0000139 }
Neil Fuller5f6750f2017-05-17 04:43:12 +0100140 }
Neil Fuller68f66662017-03-16 18:32:21 +0000141
Neil Fuller316fb6072017-07-11 11:34:25 +0100142 @GuardedBy("this")
Neil Fuller5f6750f2017-05-17 04:43:12 +0100143 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;
Neil Fuller68f66662017-03-16 18:32:21 +0000151 }
Neil Fuller5f6750f2017-05-17 04:43:12 +0100152 }
Neil Fuller68f66662017-03-16 18:32:21 +0000153
Neil Fuller5f6750f2017-05-17 04:43:12 +0100154 /** 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();
Neil Fuller68f66662017-03-16 18:32:21 +0000159
Neil Fuller316fb6072017-07-11 11:34:25 +0100160 writePackageStatusLocked(null /* status */, initialOptimisticLockId,
Neil Fuller5f6750f2017-05-17 04:43:12 +0100161 null /* packageVersions */);
162 return initialOptimisticLockId;
Neil Fuller68f66662017-03-16 18:32:21 +0000163 }
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) {
Neil Fuller5f6750f2017-05-17 04:43:12 +0100175 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");
Neil Fuller68f66662017-03-16 18:32:21 +0000180
181 // Recover.
Neil Fuller5f6750f2017-05-17 04:43:12 +0100182 optimisticLockId = recoverFromBadData(e);
Neil Fuller68f66662017-03-16 18:32:21 +0000183 }
184
185 int newOptimisticLockId = optimisticLockId + 1;
Neil Fuller5f6750f2017-05-17 04:43:12 +0100186 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);
Neil Fuller68f66662017-03-16 18:32:21 +0000197 }
Neil Fuller68f66662017-03-16 18:32:21 +0000198 }
199 }
200
201 /**
202 * Reset the current device state to "unknown".
203 */
204 void resetCheckState() {
205 synchronized(this) {
Neil Fuller5f6750f2017-05-17 04:43:12 +0100206 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");
Neil Fuller68f66662017-03-16 18:32:21 +0000212 // Attempt to recover the storage state.
Neil Fuller5f6750f2017-05-17 04:43:12 +0100213 optimisticLockId = recoverFromBadData(e);
Neil Fuller68f66662017-03-16 18:32:21 +0000214 }
215
216 int newOptimisticLockId = optimisticLockId + 1;
Neil Fuller5f6750f2017-05-17 04:43:12 +0100217 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);
Neil Fuller68f66662017-03-16 18:32:21 +0000225 }
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;
Neil Fuller5f6750f2017-05-17 04:43:12 +0100239 try {
240 return writePackageStatusWithOptimisticLockCheck(optimisticLockId,
241 newOptimisticLockId, status, checkToken.mPackageVersions);
242 } catch (IOException e) {
243 throw new IllegalStateException(e);
Neil Fuller68f66662017-03-16 18:32:21 +0000244 }
Neil Fuller68f66662017-03-16 18:32:21 +0000245 }
246 }
247
Neil Fuller316fb6072017-07-11 11:34:25 +0100248 @GuardedBy("this")
Neil Fuller5f6750f2017-05-17 04:43:12 +0100249 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
Neil Fuller316fb6072017-07-11 11:34:25 +0100283 @GuardedBy("this")
Neil Fuller5f6750f2017-05-17 04:43:12 +0100284 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
Neil Fuller316fb6072017-07-11 11:34:25 +0100299 writePackageStatusLocked(status, newOptimisticLockId, packageVersions);
Neil Fuller5f6750f2017-05-17 04:43:12 +0100300 return true;
301 }
302
Neil Fuller316fb6072017-07-11 11:34:25 +0100303 @GuardedBy("this")
304 private void writePackageStatusLocked(Integer status, int optimisticLockId,
Neil Fuller5f6750f2017-05-17 04:43:12 +0100305 PackageVersions packageVersions) throws IOException {
Neil Fuller68f66662017-03-16 18:32:21 +0000306 if ((status == null) != (packageVersions == null)) {
307 throw new IllegalArgumentException(
308 "Provide both status and packageVersions, or neither.");
309 }
310
Neil Fuller5f6750f2017-05-17 04:43:12 +0100311 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;
Neil Fuller68f66662017-03-16 18:32:21 +0000340 }
341
Neil Fuller68f66662017-03-16 18:32:21 +0000342 }
343
344 /** Only used during tests to force a known table state. */
345 public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions) {
Neil Fuller5f6750f2017-05-17 04:43:12 +0100346 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 }
Neil Fuller68f66662017-03-16 18:32:21 +0000355 }
356
Neil Fuller5f6750f2017-05-17 04:43:12 +0100357 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;
Neil Fuller68f66662017-03-16 18:32:21 +0000365 }
Neil Fuller5f6750f2017-05-17 04:43:12 +0100366 return Integer.parseInt(attributeValue);
367 } catch (NumberFormatException e) {
368 throw new ParseException(
369 "Bad integer for attributeName=" + attributeName + ": " + attributeValue, 0);
Neil Fuller68f66662017-03-16 18:32:21 +0000370 }
Neil Fuller5f6750f2017-05-17 04:43:12 +0100371 }
Neil Fuller68f66662017-03-16 18:32:21 +0000372
Neil Fuller5f6750f2017-05-17 04:43:12 +0100373 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);
Neil Fuller68f66662017-03-16 18:32:21 +0000378 }
Neil Fuller5f6750f2017-05-17 04:43:12 +0100379 return value;
Neil Fuller68f66662017-03-16 18:32:21 +0000380 }
Neil Fuller87b11282017-06-23 16:43:45 +0100381
382 public void dump(PrintWriter printWriter) {
383 printWriter.println("Package status: " + getPackageStatus());
384 }
Neil Fuller68f66662017-03-16 18:32:21 +0000385}