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