Home | History | Annotate | Download | only in packageinstaller
      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.support.annotation.NonNull;
     24 import android.support.annotation.Nullable;
     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      */
     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 {
     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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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