| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.packageinstaller; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageInstaller; |
| import android.os.AsyncTask; |
| import android.util.AtomicFile; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.util.Xml; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| import org.xmlpull.v1.XmlSerializer; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.nio.charset.StandardCharsets; |
| |
| /** |
| * Persists results of events and calls back observers when a matching result arrives. |
| */ |
| class EventResultPersister { |
| private static final String LOG_TAG = EventResultPersister.class.getSimpleName(); |
| |
| /** Id passed to {@link #addObserver(int, EventResultObserver)} to generate new id */ |
| static final int GENERATE_NEW_ID = Integer.MIN_VALUE; |
| |
| /** |
| * The extra with the id to set in the intent delivered to |
| * {@link #onEventReceived(Context, Intent)} |
| */ |
| static final String EXTRA_ID = "EventResultPersister.EXTRA_ID"; |
| |
| /** Persisted state of this object */ |
| private final AtomicFile mResultsFile; |
| |
| private final Object mLock = new Object(); |
| |
| /** Currently stored but not yet called back results (install id -> status, status message) */ |
| private final SparseArray<EventResult> mResults = new SparseArray<>(); |
| |
| /** Currently registered, not called back observers (install id -> observer) */ |
| private final SparseArray<EventResultObserver> mObservers = new SparseArray<>(); |
| |
| /** Always increasing counter for install event ids */ |
| private int mCounter; |
| |
| /** If a write that will persist the state is scheduled */ |
| private boolean mIsPersistScheduled; |
| |
| /** If the state was changed while the data was being persisted */ |
| private boolean mIsPersistingStateValid; |
| |
| /** |
| * @return a new event id. |
| */ |
| public int getNewId() throws OutOfIdsException { |
| synchronized (mLock) { |
| if (mCounter == Integer.MAX_VALUE) { |
| throw new OutOfIdsException(); |
| } |
| |
| mCounter++; |
| writeState(); |
| |
| return mCounter - 1; |
| } |
| } |
| |
| /** Call back when a result is received. Observer is removed when onResult it called. */ |
| interface EventResultObserver { |
| void onResult(int status, int legacyStatus, @Nullable String message); |
| } |
| |
| /** |
| * Progress parser to the next element. |
| * |
| * @param parser The parser to progress |
| */ |
| private static void nextElement(@NonNull XmlPullParser parser) |
| throws XmlPullParserException, IOException { |
| int type; |
| do { |
| type = parser.next(); |
| } while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT); |
| } |
| |
| /** |
| * Read an int attribute from the current element |
| * |
| * @param parser The parser to read from |
| * @param name The attribute name to read |
| * |
| * @return The value of the attribute |
| */ |
| private static int readIntAttribute(@NonNull XmlPullParser parser, @NonNull String name) { |
| return Integer.parseInt(parser.getAttributeValue(null, name)); |
| } |
| |
| /** |
| * Read an String attribute from the current element |
| * |
| * @param parser The parser to read from |
| * @param name The attribute name to read |
| * |
| * @return The value of the attribute or null if the attribute is not set |
| */ |
| private static String readStringAttribute(@NonNull XmlPullParser parser, @NonNull String name) { |
| return parser.getAttributeValue(null, name); |
| } |
| |
| /** |
| * Read persisted state. |
| * |
| * @param resultFile The file the results are persisted in |
| */ |
| EventResultPersister(@NonNull File resultFile) { |
| mResultsFile = new AtomicFile(resultFile); |
| mCounter = GENERATE_NEW_ID + 1; |
| |
| try (FileInputStream stream = mResultsFile.openRead()) { |
| XmlPullParser parser = Xml.newPullParser(); |
| parser.setInput(stream, StandardCharsets.UTF_8.name()); |
| |
| nextElement(parser); |
| while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { |
| String tagName = parser.getName(); |
| if ("results".equals(tagName)) { |
| mCounter = readIntAttribute(parser, "counter"); |
| } else if ("result".equals(tagName)) { |
| int id = readIntAttribute(parser, "id"); |
| int status = readIntAttribute(parser, "status"); |
| int legacyStatus = readIntAttribute(parser, "legacyStatus"); |
| String statusMessage = readStringAttribute(parser, "statusMessage"); |
| |
| if (mResults.get(id) != null) { |
| throw new Exception("id " + id + " has two results"); |
| } |
| |
| mResults.put(id, new EventResult(status, legacyStatus, statusMessage)); |
| } else { |
| throw new Exception("unexpected tag"); |
| } |
| |
| nextElement(parser); |
| } |
| } catch (Exception e) { |
| mResults.clear(); |
| writeState(); |
| } |
| } |
| |
| /** |
| * Add a result. If the result is an pending user action, execute the pending user action |
| * directly and do not queue a result. |
| * |
| * @param context The context the event was received in |
| * @param intent The intent the activity received |
| */ |
| void onEventReceived(@NonNull Context context, @NonNull Intent intent) { |
| int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0); |
| |
| if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) { |
| context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT)); |
| |
| return; |
| } |
| |
| int id = intent.getIntExtra(EXTRA_ID, 0); |
| String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); |
| int legacyStatus = intent.getIntExtra(PackageInstaller.EXTRA_LEGACY_STATUS, 0); |
| |
| EventResultObserver observerToCall = null; |
| synchronized (mLock) { |
| int numObservers = mObservers.size(); |
| for (int i = 0; i < numObservers; i++) { |
| if (mObservers.keyAt(i) == id) { |
| observerToCall = mObservers.valueAt(i); |
| mObservers.removeAt(i); |
| |
| break; |
| } |
| } |
| |
| if (observerToCall != null) { |
| observerToCall.onResult(status, legacyStatus, statusMessage); |
| } else { |
| mResults.put(id, new EventResult(status, legacyStatus, statusMessage)); |
| writeState(); |
| } |
| } |
| } |
| |
| /** |
| * Persist current state. The persistence might be delayed. |
| */ |
| private void writeState() { |
| synchronized (mLock) { |
| mIsPersistingStateValid = false; |
| |
| if (!mIsPersistScheduled) { |
| mIsPersistScheduled = true; |
| |
| AsyncTask.execute(() -> { |
| int counter; |
| SparseArray<EventResult> results; |
| |
| while (true) { |
| // Take snapshot of state |
| synchronized (mLock) { |
| counter = mCounter; |
| results = mResults.clone(); |
| mIsPersistingStateValid = true; |
| } |
| |
| FileOutputStream stream = null; |
| try { |
| stream = mResultsFile.startWrite(); |
| XmlSerializer serializer = Xml.newSerializer(); |
| serializer.setOutput(stream, StandardCharsets.UTF_8.name()); |
| serializer.startDocument(null, true); |
| serializer.setFeature( |
| "http://xmlpull.org/v1/doc/features.html#indent-output", true); |
| serializer.startTag(null, "results"); |
| serializer.attribute(null, "counter", Integer.toString(counter)); |
| |
| int numResults = results.size(); |
| for (int i = 0; i < numResults; i++) { |
| serializer.startTag(null, "result"); |
| serializer.attribute(null, "id", |
| Integer.toString(results.keyAt(i))); |
| serializer.attribute(null, "status", |
| Integer.toString(results.valueAt(i).status)); |
| serializer.attribute(null, "legacyStatus", |
| Integer.toString(results.valueAt(i).legacyStatus)); |
| if (results.valueAt(i).message != null) { |
| serializer.attribute(null, "statusMessage", |
| results.valueAt(i).message); |
| } |
| serializer.endTag(null, "result"); |
| } |
| |
| serializer.endTag(null, "results"); |
| serializer.endDocument(); |
| |
| mResultsFile.finishWrite(stream); |
| } catch (IOException e) { |
| if (stream != null) { |
| mResultsFile.failWrite(stream); |
| } |
| |
| Log.e(LOG_TAG, "error writing results", e); |
| mResultsFile.delete(); |
| } |
| |
| // Check if there was changed state since we persisted. If so, we need to |
| // persist again. |
| synchronized (mLock) { |
| if (mIsPersistingStateValid) { |
| mIsPersistScheduled = false; |
| break; |
| } |
| } |
| } |
| }); |
| } |
| } |
| } |
| |
| /** |
| * Add an observer. If there is already an event for this id, call back inside of this call. |
| * |
| * @param id The id the observer is for or {@code GENERATE_NEW_ID} to generate a new one. |
| * @param observer The observer to call back. |
| * |
| * @return The id for this event |
| */ |
| int addObserver(int id, @NonNull EventResultObserver observer) |
| throws OutOfIdsException { |
| synchronized (mLock) { |
| int resultIndex = -1; |
| |
| if (id == GENERATE_NEW_ID) { |
| id = getNewId(); |
| } else { |
| resultIndex = mResults.indexOfKey(id); |
| } |
| |
| // Check if we can instantly call back |
| if (resultIndex >= 0) { |
| EventResult result = mResults.valueAt(resultIndex); |
| |
| observer.onResult(result.status, result.legacyStatus, result.message); |
| mResults.removeAt(resultIndex); |
| writeState(); |
| } else { |
| mObservers.put(id, observer); |
| } |
| } |
| |
| |
| return id; |
| } |
| |
| /** |
| * Remove a observer. |
| * |
| * @param id The id the observer was added for |
| */ |
| void removeObserver(int id) { |
| synchronized (mLock) { |
| mObservers.delete(id); |
| } |
| } |
| |
| /** |
| * The status from an event. |
| */ |
| private class EventResult { |
| public final int status; |
| public final int legacyStatus; |
| @Nullable public final String message; |
| |
| private EventResult(int status, int legacyStatus, @Nullable String message) { |
| this.status = status; |
| this.legacyStatus = legacyStatus; |
| this.message = message; |
| } |
| } |
| |
| class OutOfIdsException extends Exception {} |
| } |