1 /*
2  * Copyright (C) 2016 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.packageinstaller;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.PackageInstaller;
24 import android.os.AsyncTask;
25 import android.util.AtomicFile;
26 import android.util.Log;
27 import android.util.SparseArray;
28 import android.util.Xml;
29 
30 import org.xmlpull.v1.XmlPullParser;
31 import org.xmlpull.v1.XmlPullParserException;
32 import org.xmlpull.v1.XmlSerializer;
33 
34 import java.io.File;
35 import java.io.FileInputStream;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 import java.nio.charset.StandardCharsets;
39 
40 /**
41  * Persists results of events and calls back observers when a matching result arrives.
42  */
43 class EventResultPersister {
44     private static final String LOG_TAG = EventResultPersister.class.getSimpleName();
45 
46     /** Id passed to {@link #addObserver(int, EventResultObserver)} to generate new id */
47     static final int GENERATE_NEW_ID = Integer.MIN_VALUE;
48 
49     /**
50      * The extra with the id to set in the intent delivered to
51      * {@link #onEventReceived(Context, Intent)}
52      */
53     static final String EXTRA_ID = "EventResultPersister.EXTRA_ID";
54 
55     /** Persisted state of this object */
56     private final AtomicFile mResultsFile;
57 
58     private final Object mLock = new Object();
59 
60     /** Currently stored but not yet called back results (install id -> status, status message) */
61     private final SparseArray<EventResult> mResults = new SparseArray<>();
62 
63     /** Currently registered, not called back observers (install id -> observer) */
64     private final SparseArray<EventResultObserver> mObservers = new SparseArray<>();
65 
66     /** Always increasing counter for install event ids */
67     private int mCounter;
68 
69     /** If a write that will persist the state is scheduled */
70     private boolean mIsPersistScheduled;
71 
72     /** If the state was changed while the data was being persisted */
73     private boolean mIsPersistingStateValid;
74 
75     /**
76      * @return a new event id.
77      */
getNewId()78     public int getNewId() throws OutOfIdsException {
79         synchronized (mLock) {
80             if (mCounter == Integer.MAX_VALUE) {
81                 throw new OutOfIdsException();
82             }
83 
84             mCounter++;
85             writeState();
86 
87             return mCounter - 1;
88         }
89     }
90 
91     /** Call back when a result is received. Observer is removed when onResult it called. */
92     interface EventResultObserver {
onResult(int status, int legacyStatus, @Nullable String message)93         void onResult(int status, int legacyStatus, @Nullable String message);
94     }
95 
96     /**
97      * Progress parser to the next element.
98      *
99      * @param parser The parser to progress
100      */
nextElement(@onNull XmlPullParser parser)101     private static void nextElement(@NonNull XmlPullParser parser)
102             throws XmlPullParserException, IOException {
103         int type;
104         do {
105             type = parser.next();
106         } while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT);
107     }
108 
109     /**
110      * Read an int attribute from the current element
111      *
112      * @param parser The parser to read from
113      * @param name The attribute name to read
114      *
115      * @return The value of the attribute
116      */
readIntAttribute(@onNull XmlPullParser parser, @NonNull String name)117     private static int readIntAttribute(@NonNull XmlPullParser parser, @NonNull String name) {
118         return Integer.parseInt(parser.getAttributeValue(null, name));
119     }
120 
121     /**
122      * Read an String attribute from the current element
123      *
124      * @param parser The parser to read from
125      * @param name The attribute name to read
126      *
127      * @return The value of the attribute or null if the attribute is not set
128      */
readStringAttribute(@onNull XmlPullParser parser, @NonNull String name)129     private static String readStringAttribute(@NonNull XmlPullParser parser, @NonNull String name) {
130         return parser.getAttributeValue(null, name);
131     }
132 
133     /**
134      * Read persisted state.
135      *
136      * @param resultFile The file the results are persisted in
137      */
EventResultPersister(@onNull File resultFile)138     EventResultPersister(@NonNull File resultFile) {
139         mResultsFile = new AtomicFile(resultFile);
140         mCounter = GENERATE_NEW_ID + 1;
141 
142         try (FileInputStream stream = mResultsFile.openRead()) {
143             XmlPullParser parser = Xml.newPullParser();
144             parser.setInput(stream, StandardCharsets.UTF_8.name());
145 
146             nextElement(parser);
147             while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
148                 String tagName = parser.getName();
149                 if ("results".equals(tagName)) {
150                     mCounter = readIntAttribute(parser, "counter");
151                 } else if ("result".equals(tagName)) {
152                     int id = readIntAttribute(parser, "id");
153                     int status = readIntAttribute(parser, "status");
154                     int legacyStatus = readIntAttribute(parser, "legacyStatus");
155                     String statusMessage = readStringAttribute(parser, "statusMessage");
156 
157                     if (mResults.get(id) != null) {
158                         throw new Exception("id " + id + " has two results");
159                     }
160 
161                     mResults.put(id, new EventResult(status, legacyStatus, statusMessage));
162                 } else {
163                     throw new Exception("unexpected tag");
164                 }
165 
166                 nextElement(parser);
167             }
168         } catch (Exception e) {
169             mResults.clear();
170             writeState();
171         }
172     }
173 
174     /**
175      * Add a result. If the result is an pending user action, execute the pending user action
176      * directly and do not queue a result.
177      *
178      * @param context The context the event was received in
179      * @param intent The intent the activity received
180      */
onEventReceived(@onNull Context context, @NonNull Intent intent)181     void onEventReceived(@NonNull Context context, @NonNull Intent intent) {
182         int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
183 
184         if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
185             context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT));
186 
187             return;
188         }
189 
190         int id = intent.getIntExtra(EXTRA_ID, 0);
191         String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
192         int legacyStatus = intent.getIntExtra(PackageInstaller.EXTRA_LEGACY_STATUS, 0);
193 
194         EventResultObserver observerToCall = null;
195         synchronized (mLock) {
196             int numObservers = mObservers.size();
197             for (int i = 0; i < numObservers; i++) {
198                 if (mObservers.keyAt(i) == id) {
199                     observerToCall = mObservers.valueAt(i);
200                     mObservers.removeAt(i);
201 
202                     break;
203                 }
204             }
205 
206             if (observerToCall != null) {
207                 observerToCall.onResult(status, legacyStatus, statusMessage);
208             } else {
209                 mResults.put(id, new EventResult(status, legacyStatus, statusMessage));
210                 writeState();
211             }
212         }
213     }
214 
215     /**
216      * Persist current state. The persistence might be delayed.
217      */
writeState()218     private void writeState() {
219         synchronized (mLock) {
220             mIsPersistingStateValid = false;
221 
222             if (!mIsPersistScheduled) {
223                 mIsPersistScheduled = true;
224 
225                 AsyncTask.execute(() -> {
226                     int counter;
227                     SparseArray<EventResult> results;
228 
229                     while (true) {
230                         // Take snapshot of state
231                         synchronized (mLock) {
232                             counter = mCounter;
233                             results = mResults.clone();
234                             mIsPersistingStateValid = true;
235                         }
236 
237                         FileOutputStream stream = null;
238                         try {
239                             stream = mResultsFile.startWrite();
240                             XmlSerializer serializer = Xml.newSerializer();
241                             serializer.setOutput(stream, StandardCharsets.UTF_8.name());
242                             serializer.startDocument(null, true);
243                             serializer.setFeature(
244                                     "http://xmlpull.org/v1/doc/features.html#indent-output", true);
245                             serializer.startTag(null, "results");
246                             serializer.attribute(null, "counter", Integer.toString(counter));
247 
248                             int numResults = results.size();
249                             for (int i = 0; i < numResults; i++) {
250                                 serializer.startTag(null, "result");
251                                 serializer.attribute(null, "id",
252                                         Integer.toString(results.keyAt(i)));
253                                 serializer.attribute(null, "status",
254                                         Integer.toString(results.valueAt(i).status));
255                                 serializer.attribute(null, "legacyStatus",
256                                         Integer.toString(results.valueAt(i).legacyStatus));
257                                 if (results.valueAt(i).message != null) {
258                                     serializer.attribute(null, "statusMessage",
259                                             results.valueAt(i).message);
260                                 }
261                                 serializer.endTag(null, "result");
262                             }
263 
264                             serializer.endTag(null, "results");
265                             serializer.endDocument();
266 
267                             mResultsFile.finishWrite(stream);
268                         } catch (IOException e) {
269                             if (stream != null) {
270                                 mResultsFile.failWrite(stream);
271                             }
272 
273                             Log.e(LOG_TAG, "error writing results", e);
274                             mResultsFile.delete();
275                         }
276 
277                         // Check if there was changed state since we persisted. If so, we need to
278                         // persist again.
279                         synchronized (mLock) {
280                             if (mIsPersistingStateValid) {
281                                 mIsPersistScheduled = false;
282                                 break;
283                             }
284                         }
285                     }
286                 });
287             }
288         }
289     }
290 
291     /**
292      * Add an observer. If there is already an event for this id, call back inside of this call.
293      *
294      * @param id       The id the observer is for or {@code GENERATE_NEW_ID} to generate a new one.
295      * @param observer The observer to call back.
296      *
297      * @return The id for this event
298      */
addObserver(int id, @NonNull EventResultObserver observer)299     int addObserver(int id, @NonNull EventResultObserver observer)
300             throws OutOfIdsException {
301         synchronized (mLock) {
302             int resultIndex = -1;
303 
304             if (id == GENERATE_NEW_ID) {
305                 id = getNewId();
306             } else {
307                 resultIndex = mResults.indexOfKey(id);
308             }
309 
310             // Check if we can instantly call back
311             if (resultIndex >= 0) {
312                 EventResult result = mResults.valueAt(resultIndex);
313 
314                 observer.onResult(result.status, result.legacyStatus, result.message);
315                 mResults.removeAt(resultIndex);
316                 writeState();
317             } else {
318                 mObservers.put(id, observer);
319             }
320         }
321 
322 
323         return id;
324     }
325 
326     /**
327      * Remove a observer.
328      *
329      * @param id The id the observer was added for
330      */
removeObserver(int id)331     void removeObserver(int id) {
332         synchronized (mLock) {
333             mObservers.delete(id);
334         }
335     }
336 
337     /**
338      * The status from an event.
339      */
340     private class EventResult {
341         public final int status;
342         public final int legacyStatus;
343         @Nullable public final String message;
344 
EventResult(int status, int legacyStatus, @Nullable String message)345         private EventResult(int status, int legacyStatus, @Nullable String message) {
346             this.status = status;
347             this.legacyStatus = legacyStatus;
348             this.message = message;
349         }
350     }
351 
352     class OutOfIdsException extends Exception {}
353 }
354