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