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