• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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