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