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