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