blob: 04f0871c98973889fdfe64dacf86a08e75bbb521 [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
Dianne Hackborn3accca02013-09-20 09:32:11 -070079 private static final long UNKNOWN_PACKAGE_VERSION = -1;
Neil Fuller68f66662017-03-16 18:32:21 +000080
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) {
Dianne Hackborne17b4452018-01-10 13:15:40 -080084 mPackageStatusFile = new AtomicFile(new File(storageDir, "package-status.xml"), "timezone-status");
Neil Fuller35822592017-12-11 14:39:03 +000085 }
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 {
Neil Fuller5f6750f2017-05-17 04:43:12 +010093 if (!mPackageStatusFile.getBaseFile().exists()) {
Neil Fuller35822592017-12-11 14:39:03 +000094 insertInitialPackageStatus();
Neil Fuller5f6750f2017-05-17 04:43:12 +010095 }
Neil Fuller68f66662017-03-16 18:32:21 +000096 }
97
Neil Fuller5f6750f2017-05-17 04:43:12 +010098 void deleteFileForTests() {
99 synchronized(this) {
100 mPackageStatusFile.delete();
101 }
Neil Fuller68f66662017-03-16 18:32:21 +0000102 }
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 {
Neil Fuller316fb6072017-07-11 11:34:25 +0100111 return getPackageStatusLocked();
Neil Fuller5f6750f2017-05-17 04:43:12 +0100112 } 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);
Neil Fuller68f66662017-03-16 18:32:21 +0000115
116 // Reset the storage so it is in a good state again.
Neil Fuller5f6750f2017-05-17 04:43:12 +0100117 recoverFromBadData(e);
118 try {
Neil Fuller316fb6072017-07-11 11:34:25 +0100119 return getPackageStatusLocked();
Neil Fuller5f6750f2017-05-17 04:43:12 +0100120 } catch (ParseException e2) {
121 throw new IllegalStateException("Recovery from bad file failed", e2);
122 }
Neil Fuller68f66662017-03-16 18:32:21 +0000123 }
124 }
125 }
126
Neil Fuller316fb6072017-07-11 11:34:25 +0100127 @GuardedBy("this")
128 private PackageStatus getPackageStatusLocked() throws ParseException {
Neil Fuller5f6750f2017-05-17 04:43:12 +0100129 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;
Neil Fuller68f66662017-03-16 18:32:21 +0000143 }
Neil Fuller5f6750f2017-05-17 04:43:12 +0100144 }
Neil Fuller68f66662017-03-16 18:32:21 +0000145
Neil Fuller316fb6072017-07-11 11:34:25 +0100146 @GuardedBy("this")
Neil Fuller5f6750f2017-05-17 04:43:12 +0100147 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;
Neil Fuller68f66662017-03-16 18:32:21 +0000155 }
Neil Fuller5f6750f2017-05-17 04:43:12 +0100156 }
Neil Fuller68f66662017-03-16 18:32:21 +0000157
Neil Fuller5f6750f2017-05-17 04:43:12 +0100158 /** 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();
Neil Fuller68f66662017-03-16 18:32:21 +0000163
Neil Fuller316fb6072017-07-11 11:34:25 +0100164 writePackageStatusLocked(null /* status */, initialOptimisticLockId,
Neil Fuller5f6750f2017-05-17 04:43:12 +0100165 null /* packageVersions */);
166 return initialOptimisticLockId;
Neil Fuller68f66662017-03-16 18:32:21 +0000167 }
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) {
Neil Fuller5f6750f2017-05-17 04:43:12 +0100179 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");
Neil Fuller68f66662017-03-16 18:32:21 +0000184
185 // Recover.
Neil Fuller5f6750f2017-05-17 04:43:12 +0100186 optimisticLockId = recoverFromBadData(e);
Neil Fuller68f66662017-03-16 18:32:21 +0000187 }
188
189 int newOptimisticLockId = optimisticLockId + 1;
Neil Fuller5f6750f2017-05-17 04:43:12 +0100190 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);
Neil Fuller68f66662017-03-16 18:32:21 +0000201 }
Neil Fuller68f66662017-03-16 18:32:21 +0000202 }
203 }
204
205 /**
206 * Reset the current device state to "unknown".
207 */
208 void resetCheckState() {
209 synchronized(this) {
Neil Fuller5f6750f2017-05-17 04:43:12 +0100210 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");
Neil Fuller68f66662017-03-16 18:32:21 +0000216 // Attempt to recover the storage state.
Neil Fuller5f6750f2017-05-17 04:43:12 +0100217 optimisticLockId = recoverFromBadData(e);
Neil Fuller68f66662017-03-16 18:32:21 +0000218 }
219
220 int newOptimisticLockId = optimisticLockId + 1;
Neil Fuller5f6750f2017-05-17 04:43:12 +0100221 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);
Neil Fuller68f66662017-03-16 18:32:21 +0000229 }
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;
Neil Fuller5f6750f2017-05-17 04:43:12 +0100243 try {
244 return writePackageStatusWithOptimisticLockCheck(optimisticLockId,
245 newOptimisticLockId, status, checkToken.mPackageVersions);
246 } catch (IOException e) {
247 throw new IllegalStateException(e);
Neil Fuller68f66662017-03-16 18:32:21 +0000248 }
Neil Fuller68f66662017-03-16 18:32:21 +0000249 }
250 }
251
Neil Fuller316fb6072017-07-11 11:34:25 +0100252 @GuardedBy("this")
Neil Fuller5f6750f2017-05-17 04:43:12 +0100253 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
Neil Fuller316fb6072017-07-11 11:34:25 +0100287 @GuardedBy("this")
Neil Fuller5f6750f2017-05-17 04:43:12 +0100288 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
Neil Fuller316fb6072017-07-11 11:34:25 +0100303 writePackageStatusLocked(status, newOptimisticLockId, packageVersions);
Neil Fuller5f6750f2017-05-17 04:43:12 +0100304 return true;
305 }
306
Neil Fuller316fb6072017-07-11 11:34:25 +0100307 @GuardedBy("this")
308 private void writePackageStatusLocked(Integer status, int optimisticLockId,
Neil Fuller5f6750f2017-05-17 04:43:12 +0100309 PackageVersions packageVersions) throws IOException {
Neil Fuller68f66662017-03-16 18:32:21 +0000310 if ((status == null) != (packageVersions == null)) {
311 throw new IllegalArgumentException(
312 "Provide both status and packageVersions, or neither.");
313 }
314
Neil Fuller5f6750f2017-05-17 04:43:12 +0100315 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));
Dianne Hackborn3accca02013-09-20 09:32:11 -0700327 long updateAppVersion = status == null
Neil Fuller5f6750f2017-05-17 04:43:12 +0100328 ? UNKNOWN_PACKAGE_VERSION : packageVersions.mUpdateAppVersion;
329 serializer.attribute(namespace, ATTRIBUTE_UPDATE_APP_VERSION,
Dianne Hackborn3accca02013-09-20 09:32:11 -0700330 Long.toString(updateAppVersion));
331 long dataAppVersion = status == null
Neil Fuller5f6750f2017-05-17 04:43:12 +0100332 ? UNKNOWN_PACKAGE_VERSION : packageVersions.mDataAppVersion;
333 serializer.attribute(namespace, ATTRIBUTE_DATA_APP_VERSION,
Dianne Hackborn3accca02013-09-20 09:32:11 -0700334 Long.toString(dataAppVersion));
Neil Fuller5f6750f2017-05-17 04:43:12 +0100335 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;
Neil Fuller68f66662017-03-16 18:32:21 +0000344 }
345
Neil Fuller68f66662017-03-16 18:32:21 +0000346 }
347
348 /** Only used during tests to force a known table state. */
Neil Fuller35822592017-12-11 14:39:03 +0000349 public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions)
350 throws IOException {
Neil Fuller5f6750f2017-05-17 04:43:12 +0100351 synchronized (this) {
352 try {
Neil Fuller35822592017-12-11 14:39:03 +0000353 final int initialOptimisticLockId = (int) System.currentTimeMillis();
354 writePackageStatusLocked(checkStatus, initialOptimisticLockId, packageVersions);
355 } catch (IOException e) {
Neil Fuller5f6750f2017-05-17 04:43:12 +0100356 throw new IllegalStateException(e);
357 }
358 }
Neil Fuller68f66662017-03-16 18:32:21 +0000359 }
360
Neil Fuller5f6750f2017-05-17 04:43:12 +0100361 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;
Neil Fuller68f66662017-03-16 18:32:21 +0000369 }
Neil Fuller5f6750f2017-05-17 04:43:12 +0100370 return Integer.parseInt(attributeValue);
371 } catch (NumberFormatException e) {
372 throw new ParseException(
373 "Bad integer for attributeName=" + attributeName + ": " + attributeValue, 0);
Neil Fuller68f66662017-03-16 18:32:21 +0000374 }
Neil Fuller5f6750f2017-05-17 04:43:12 +0100375 }
Neil Fuller68f66662017-03-16 18:32:21 +0000376
Neil Fuller5f6750f2017-05-17 04:43:12 +0100377 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);
Neil Fuller68f66662017-03-16 18:32:21 +0000382 }
Neil Fuller5f6750f2017-05-17 04:43:12 +0100383 return value;
Neil Fuller68f66662017-03-16 18:32:21 +0000384 }
Neil Fuller87b11282017-06-23 16:43:45 +0100385
386 public void dump(PrintWriter printWriter) {
387 printWriter.println("Package status: " + getPackageStatus());
388 }
Neil Fuller68f66662017-03-16 18:32:21 +0000389}