Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2013 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.printspooler.ui;
     18 
     19 import android.content.ComponentName;
     20 import android.content.Context;
     21 import android.content.Loader;
     22 import android.content.pm.ServiceInfo;
     23 import android.os.AsyncTask;
     24 import android.print.PrintManager;
     25 import android.print.PrinterDiscoverySession;
     26 import android.print.PrinterDiscoverySession.OnPrintersChangeListener;
     27 import android.print.PrinterId;
     28 import android.print.PrinterInfo;
     29 import android.printservice.PrintServiceInfo;
     30 import android.text.TextUtils;
     31 import android.util.ArrayMap;
     32 import android.util.ArraySet;
     33 import android.util.AtomicFile;
     34 import android.util.Log;
     35 import android.util.Slog;
     36 import android.util.Xml;
     37 
     38 import com.android.internal.util.FastXmlSerializer;
     39 
     40 import org.xmlpull.v1.XmlPullParser;
     41 import org.xmlpull.v1.XmlPullParserException;
     42 import org.xmlpull.v1.XmlSerializer;
     43 
     44 import java.io.File;
     45 import java.io.FileInputStream;
     46 import java.io.FileNotFoundException;
     47 import java.io.FileOutputStream;
     48 import java.io.IOException;
     49 import java.util.ArrayList;
     50 import java.util.Collections;
     51 import java.util.LinkedHashMap;
     52 import java.util.List;
     53 import java.util.Map;
     54 import java.util.Set;
     55 
     56 import libcore.io.IoUtils;
     57 
     58 /**
     59  * This class is responsible for loading printers by doing discovery
     60  * and merging the discovered printers with the previously used ones.
     61  */
     62 public final class FusedPrintersProvider extends Loader<List<PrinterInfo>> {
     63     private static final String LOG_TAG = "FusedPrintersProvider";
     64 
     65     private static final boolean DEBUG = false;
     66 
     67     private static final double WEIGHT_DECAY_COEFFICIENT = 0.95f;
     68     private static final int MAX_HISTORY_LENGTH = 50;
     69 
     70     private static final int MAX_FAVORITE_PRINTER_COUNT = 4;
     71 
     72     private final List<PrinterInfo> mPrinters =
     73             new ArrayList<>();
     74 
     75     private final List<PrinterInfo> mFavoritePrinters =
     76             new ArrayList<>();
     77 
     78     private final PersistenceManager mPersistenceManager;
     79 
     80     private PrinterDiscoverySession mDiscoverySession;
     81 
     82     private PrinterId mTrackedPrinter;
     83 
     84     private boolean mPrintersUpdatedBefore;
     85 
     86     public FusedPrintersProvider(Context context) {
     87         super(context);
     88         mPersistenceManager = new PersistenceManager(context);
     89     }
     90 
     91     public void addHistoricalPrinter(PrinterInfo printer) {
     92         mPersistenceManager.addPrinterAndWritePrinterHistory(printer);
     93     }
     94 
     95     private void computeAndDeliverResult(Map<PrinterId, PrinterInfo> discoveredPrinters,
     96             List<PrinterInfo> favoritePrinters) {
     97         List<PrinterInfo> printers = new ArrayList<>();
     98 
     99         // Add the updated favorite printers.
    100         final int favoritePrinterCount = favoritePrinters.size();
    101         for (int i = 0; i < favoritePrinterCount; i++) {
    102             PrinterInfo favoritePrinter = favoritePrinters.get(i);
    103             PrinterInfo updatedPrinter = discoveredPrinters.remove(
    104                     favoritePrinter.getId());
    105             if (updatedPrinter != null) {
    106                 printers.add(updatedPrinter);
    107             } else {
    108                 printers.add(favoritePrinter);
    109             }
    110         }
    111 
    112         // Add other updated printers.
    113         final int printerCount = mPrinters.size();
    114         for (int i = 0; i < printerCount; i++) {
    115             PrinterInfo printer = mPrinters.get(i);
    116             PrinterInfo updatedPrinter = discoveredPrinters.remove(
    117                     printer.getId());
    118             if (updatedPrinter != null) {
    119                 printers.add(updatedPrinter);
    120             }
    121         }
    122 
    123         // Add the new printers, i.e. what is left.
    124         printers.addAll(discoveredPrinters.values());
    125 
    126         // Update the list of printers.
    127         mPrinters.clear();
    128         mPrinters.addAll(printers);
    129 
    130         if (isStarted()) {
    131             // If stated deliver the new printers.
    132             deliverResult(printers);
    133         } else {
    134             // Otherwise, take a note for the change.
    135             onContentChanged();
    136         }
    137     }
    138 
    139     @Override
    140     protected void onStartLoading() {
    141         if (DEBUG) {
    142             Log.i(LOG_TAG, "onStartLoading() " + FusedPrintersProvider.this.hashCode());
    143         }
    144         // The contract is that if we already have a valid,
    145         // result the we have to deliver it immediately.
    146         if (!mPrinters.isEmpty()) {
    147             deliverResult(new ArrayList<>(mPrinters));
    148         }
    149         // Always load the data to ensure discovery period is
    150         // started and to make sure obsolete printers are updated.
    151         onForceLoad();
    152     }
    153 
    154     @Override
    155     protected void onStopLoading() {
    156         if (DEBUG) {
    157             Log.i(LOG_TAG, "onStopLoading() " + FusedPrintersProvider.this.hashCode());
    158         }
    159         onCancelLoad();
    160     }
    161 
    162     @Override
    163     protected void onForceLoad() {
    164         if (DEBUG) {
    165             Log.i(LOG_TAG, "onForceLoad() " + FusedPrintersProvider.this.hashCode());
    166         }
    167         loadInternal();
    168     }
    169 
    170     private void loadInternal() {
    171         if (mDiscoverySession == null) {
    172             PrintManager printManager = (PrintManager) getContext()
    173                     .getSystemService(Context.PRINT_SERVICE);
    174             mDiscoverySession = printManager.createPrinterDiscoverySession();
    175             mPersistenceManager.readPrinterHistory();
    176         } else if (mPersistenceManager.isHistoryChanged()) {
    177             mPersistenceManager.readPrinterHistory();
    178         }
    179         if (mPersistenceManager.isReadHistoryCompleted()
    180                 && !mDiscoverySession.isPrinterDiscoveryStarted()) {
    181             mDiscoverySession.setOnPrintersChangeListener(new OnPrintersChangeListener() {
    182                 @Override
    183                 public void onPrintersChanged() {
    184                     if (DEBUG) {
    185                         Log.i(LOG_TAG, "onPrintersChanged() count:"
    186                                 + mDiscoverySession.getPrinters().size()
    187                                 + " " + FusedPrintersProvider.this.hashCode());
    188                     }
    189 
    190                     updatePrinters(mDiscoverySession.getPrinters(), mFavoritePrinters);
    191                 }
    192             });
    193             final int favoriteCount = mFavoritePrinters.size();
    194             List<PrinterId> printerIds = new ArrayList<>(favoriteCount);
    195             for (int i = 0; i < favoriteCount; i++) {
    196                 printerIds.add(mFavoritePrinters.get(i).getId());
    197             }
    198             mDiscoverySession.startPrinterDiscovery(printerIds);
    199             List<PrinterInfo> printers = mDiscoverySession.getPrinters();
    200             if (!printers.isEmpty()) {
    201                 updatePrinters(printers, mFavoritePrinters);
    202             }
    203         }
    204     }
    205 
    206     private void updatePrinters(List<PrinterInfo> printers, List<PrinterInfo> favoritePrinters) {
    207         if (mPrintersUpdatedBefore && mPrinters.equals(printers)
    208                 && mFavoritePrinters.equals(favoritePrinters)) {
    209             return;
    210         }
    211 
    212         mPrintersUpdatedBefore = true;
    213 
    214         // Some of the found printers may have be a printer that is in the
    215         // history but with its name changed. Hence, we try to update the
    216         // printer to use its current name instead of the historical one.
    217         mPersistenceManager.updatePrintersHistoricalNamesIfNeeded(printers);
    218 
    219         Map<PrinterId, PrinterInfo> printersMap = new LinkedHashMap<>();
    220         final int printerCount = printers.size();
    221         for (int i = 0; i < printerCount; i++) {
    222             PrinterInfo printer = printers.get(i);
    223             printersMap.put(printer.getId(), printer);
    224         }
    225 
    226         computeAndDeliverResult(printersMap, favoritePrinters);
    227     }
    228 
    229     @Override
    230     protected boolean onCancelLoad() {
    231         if (DEBUG) {
    232             Log.i(LOG_TAG, "onCancelLoad() " + FusedPrintersProvider.this.hashCode());
    233         }
    234         return cancelInternal();
    235     }
    236 
    237     private boolean cancelInternal() {
    238         if (mDiscoverySession != null
    239                 && mDiscoverySession.isPrinterDiscoveryStarted()) {
    240             if (mTrackedPrinter != null) {
    241                 mDiscoverySession.stopPrinterStateTracking(mTrackedPrinter);
    242                 mTrackedPrinter = null;
    243             }
    244             mDiscoverySession.stopPrinterDiscovery();
    245             return true;
    246         } else if (mPersistenceManager.isReadHistoryInProgress()) {
    247             return mPersistenceManager.stopReadPrinterHistory();
    248         }
    249         return false;
    250     }
    251 
    252     @Override
    253     protected void onReset() {
    254         if (DEBUG) {
    255             Log.i(LOG_TAG, "onReset() " + FusedPrintersProvider.this.hashCode());
    256         }
    257         onStopLoading();
    258         mPrinters.clear();
    259         if (mDiscoverySession != null) {
    260             mDiscoverySession.destroy();
    261             mDiscoverySession = null;
    262         }
    263     }
    264 
    265     @Override
    266     protected void onAbandon() {
    267         if (DEBUG) {
    268             Log.i(LOG_TAG, "onAbandon() " + FusedPrintersProvider.this.hashCode());
    269         }
    270         onStopLoading();
    271     }
    272 
    273     public boolean areHistoricalPrintersLoaded() {
    274         return mPersistenceManager.mReadHistoryCompleted;
    275     }
    276 
    277     public void setTrackedPrinter(PrinterId printerId) {
    278         if (isStarted() && mDiscoverySession != null
    279                 && mDiscoverySession.isPrinterDiscoveryStarted()) {
    280             if (mTrackedPrinter != null) {
    281                 if (mTrackedPrinter.equals(printerId)) {
    282                     return;
    283                 }
    284                 mDiscoverySession.stopPrinterStateTracking(mTrackedPrinter);
    285             }
    286             mTrackedPrinter = printerId;
    287             if (printerId != null) {
    288                 mDiscoverySession.startPrinterStateTracking(printerId);
    289             }
    290         }
    291     }
    292 
    293     public boolean isFavoritePrinter(PrinterId printerId) {
    294         final int printerCount = mFavoritePrinters.size();
    295         for (int i = 0; i < printerCount; i++) {
    296             PrinterInfo favoritePritner = mFavoritePrinters.get(i);
    297             if (favoritePritner.getId().equals(printerId)) {
    298                 return true;
    299             }
    300         }
    301         return false;
    302     }
    303 
    304     public void forgetFavoritePrinter(PrinterId printerId) {
    305         List<PrinterInfo> newFavoritePrinters = null;
    306 
    307         // Remove the printer from the favorites.
    308         final int favoritePrinterCount = mFavoritePrinters.size();
    309         for (int i = 0; i < favoritePrinterCount; i++) {
    310             PrinterInfo favoritePrinter = mFavoritePrinters.get(i);
    311             if (favoritePrinter.getId().equals(printerId)) {
    312                 newFavoritePrinters = new ArrayList<>();
    313                 newFavoritePrinters.addAll(mPrinters);
    314                 newFavoritePrinters.remove(i);
    315                 break;
    316             }
    317         }
    318 
    319         // If we removed a favorite printer, we have work to do.
    320         if (newFavoritePrinters != null) {
    321             // Remove the printer from history and persist the latter.
    322             mPersistenceManager.removeHistoricalPrinterAndWritePrinterHistory(printerId);
    323 
    324             // Recompute and deliver the printers.
    325             updatePrinters(mDiscoverySession.getPrinters(), newFavoritePrinters);
    326         }
    327     }
    328 
    329     private final class PersistenceManager {
    330         private static final String PERSIST_FILE_NAME = "printer_history.xml";
    331 
    332         private static final String TAG_PRINTERS = "printers";
    333 
    334         private static final String TAG_PRINTER = "printer";
    335         private static final String TAG_PRINTER_ID = "printerId";
    336 
    337         private static final String ATTR_LOCAL_ID = "localId";
    338         private static final String ATTR_SERVICE_NAME = "serviceName";
    339 
    340         private static final String ATTR_NAME = "name";
    341         private static final String ATTR_DESCRIPTION = "description";
    342         private static final String ATTR_STATUS = "status";
    343 
    344         private final AtomicFile mStatePersistFile;
    345 
    346         private List<PrinterInfo> mHistoricalPrinters = new ArrayList<>();
    347 
    348         private boolean mReadHistoryCompleted;
    349 
    350         private ReadTask mReadTask;
    351 
    352         private volatile long mLastReadHistoryTimestamp;
    353 
    354         private PersistenceManager(Context context) {
    355             mStatePersistFile = new AtomicFile(new File(context.getFilesDir(),
    356                     PERSIST_FILE_NAME));
    357         }
    358 
    359         public boolean isReadHistoryInProgress() {
    360             return mReadTask != null;
    361         }
    362 
    363         public boolean isReadHistoryCompleted() {
    364             return mReadHistoryCompleted;
    365         }
    366 
    367         public boolean stopReadPrinterHistory() {
    368             return mReadTask.cancel(true);
    369         }
    370 
    371         public void readPrinterHistory() {
    372             if (DEBUG) {
    373                 Log.i(LOG_TAG, "read history started "
    374                         + FusedPrintersProvider.this.hashCode());
    375             }
    376             mReadTask = new ReadTask();
    377             mReadTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
    378         }
    379 
    380         public void updatePrintersHistoricalNamesIfNeeded(List<PrinterInfo> printers) {
    381             boolean writeHistory = false;
    382 
    383             final int printerCount = printers.size();
    384             for (int i = 0; i < printerCount; i++) {
    385                 PrinterInfo printer = printers.get(i);
    386                 writeHistory |= renamePrinterIfNeeded(printer);
    387             }
    388 
    389             if (writeHistory) {
    390                 writePrinterHistory();
    391             }
    392         }
    393 
    394         public boolean renamePrinterIfNeeded(PrinterInfo printer) {
    395             boolean renamed = false;
    396             final int printerCount = mHistoricalPrinters.size();
    397             for (int i = 0; i < printerCount; i++) {
    398                 PrinterInfo historicalPrinter = mHistoricalPrinters.get(i);
    399                 if (historicalPrinter.getId().equals(printer.getId())
    400                         && !TextUtils.equals(historicalPrinter.getName(), printer.getName())) {
    401                     mHistoricalPrinters.set(i, printer);
    402                     renamed = true;
    403                 }
    404             }
    405             return renamed;
    406         }
    407 
    408         public void addPrinterAndWritePrinterHistory(PrinterInfo printer) {
    409             if (mHistoricalPrinters.size() >= MAX_HISTORY_LENGTH) {
    410                 mHistoricalPrinters.remove(0);
    411             }
    412             mHistoricalPrinters.add(printer);
    413             writePrinterHistory();
    414         }
    415 
    416         public void removeHistoricalPrinterAndWritePrinterHistory(PrinterId printerId) {
    417             boolean writeHistory = false;
    418             final int printerCount = mHistoricalPrinters.size();
    419             for (int i = printerCount - 1; i >= 0; i--) {
    420                 PrinterInfo historicalPrinter = mHistoricalPrinters.get(i);
    421                 if (historicalPrinter.getId().equals(printerId)) {
    422                     mHistoricalPrinters.remove(i);
    423                     writeHistory = true;
    424                 }
    425             }
    426             if (writeHistory) {
    427                 writePrinterHistory();
    428             }
    429         }
    430 
    431         @SuppressWarnings("unchecked")
    432         private void writePrinterHistory() {
    433             new WriteTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,
    434                     new ArrayList<>(mHistoricalPrinters));
    435         }
    436 
    437         public boolean isHistoryChanged() {
    438             return mLastReadHistoryTimestamp != mStatePersistFile.getBaseFile().lastModified();
    439         }
    440 
    441         private List<PrinterInfo> computeFavoritePrinters(List<PrinterInfo> printers) {
    442             Map<PrinterId, PrinterRecord> recordMap = new ArrayMap<>();
    443 
    444             // Recompute the weights.
    445             float currentWeight = 1.0f;
    446             final int printerCount = printers.size();
    447             for (int i = printerCount - 1; i >= 0; i--) {
    448                 PrinterInfo printer = printers.get(i);
    449                 // Aggregate weight for the same printer
    450                 PrinterRecord record = recordMap.get(printer.getId());
    451                 if (record == null) {
    452                     record = new PrinterRecord(printer);
    453                     recordMap.put(printer.getId(), record);
    454                 }
    455                 record.weight += currentWeight;
    456                 currentWeight *= WEIGHT_DECAY_COEFFICIENT;
    457             }
    458 
    459             // Soft the favorite printers.
    460             List<PrinterRecord> favoriteRecords = new ArrayList<>(
    461                     recordMap.values());
    462             Collections.sort(favoriteRecords);
    463 
    464             // Write the favorites to the output.
    465             final int favoriteCount = Math.min(favoriteRecords.size(),
    466                     MAX_FAVORITE_PRINTER_COUNT);
    467             List<PrinterInfo> favoritePrinters = new ArrayList<>(favoriteCount);
    468             for (int i = 0; i < favoriteCount; i++) {
    469                 PrinterInfo printer = favoriteRecords.get(i).printer;
    470                 favoritePrinters.add(printer);
    471             }
    472 
    473             return favoritePrinters;
    474         }
    475 
    476         private final class PrinterRecord implements Comparable<PrinterRecord> {
    477             public final PrinterInfo printer;
    478             public float weight;
    479 
    480             public PrinterRecord(PrinterInfo printer) {
    481                 this.printer = printer;
    482             }
    483 
    484             @Override
    485             public int compareTo(PrinterRecord another) {
    486                 return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
    487             }
    488         }
    489 
    490         private final class ReadTask extends AsyncTask<Void, Void, List<PrinterInfo>> {
    491             @Override
    492             protected List<PrinterInfo> doInBackground(Void... args) {
    493                return doReadPrinterHistory();
    494             }
    495 
    496             @Override
    497             protected void onPostExecute(List<PrinterInfo> printers) {
    498                 if (DEBUG) {
    499                     Log.i(LOG_TAG, "read history completed "
    500                             + FusedPrintersProvider.this.hashCode());
    501                 }
    502 
    503                 // Ignore printer records whose target services are not enabled.
    504                 PrintManager printManager = (PrintManager) getContext()
    505                         .getSystemService(Context.PRINT_SERVICE);
    506                 List<PrintServiceInfo> services = printManager
    507                         .getEnabledPrintServices();
    508 
    509                 Set<ComponentName> enabledComponents = new ArraySet<>();
    510                 final int installedServiceCount = services.size();
    511                 for (int i = 0; i < installedServiceCount; i++) {
    512                     ServiceInfo serviceInfo = services.get(i).getResolveInfo().serviceInfo;
    513                     ComponentName componentName = new ComponentName(
    514                             serviceInfo.packageName, serviceInfo.name);
    515                     enabledComponents.add(componentName);
    516                 }
    517 
    518                 final int printerCount = printers.size();
    519                 for (int i = printerCount - 1; i >= 0; i--) {
    520                     ComponentName printerServiceName = printers.get(i).getId().getServiceName();
    521                     if (!enabledComponents.contains(printerServiceName)) {
    522                         printers.remove(i);
    523                     }
    524                 }
    525 
    526                 // Store the filtered list.
    527                 mHistoricalPrinters = printers;
    528 
    529                 // Compute the favorite printers.
    530                 mFavoritePrinters.clear();
    531                 mFavoritePrinters.addAll(computeFavoritePrinters(mHistoricalPrinters));
    532 
    533                 mReadHistoryCompleted = true;
    534 
    535                 // Deliver the printers.
    536                 updatePrinters(mDiscoverySession.getPrinters(), mFavoritePrinters);
    537 
    538                 // We are done.
    539                 mReadTask = null;
    540 
    541                 // Loading the available printers if needed.
    542                 loadInternal();
    543             }
    544 
    545             @Override
    546             protected void onCancelled(List<PrinterInfo> printerInfos) {
    547                 // We are done.
    548                 mReadTask = null;
    549             }
    550 
    551             private List<PrinterInfo> doReadPrinterHistory() {
    552                 final FileInputStream in;
    553                 try {
    554                     in = mStatePersistFile.openRead();
    555                 } catch (FileNotFoundException fnfe) {
    556                     if (DEBUG) {
    557                         Log.i(LOG_TAG, "No existing printer history "
    558                                 + FusedPrintersProvider.this.hashCode());
    559                     }
    560                     return new ArrayList<>();
    561                 }
    562                 try {
    563                     List<PrinterInfo> printers = new ArrayList<>();
    564                     XmlPullParser parser = Xml.newPullParser();
    565                     parser.setInput(in, null);
    566                     parseState(parser, printers);
    567                     // Take a note which version of the history was read.
    568                     mLastReadHistoryTimestamp = mStatePersistFile.getBaseFile().lastModified();
    569                     return printers;
    570                 } catch (IllegalStateException
    571                         | NullPointerException
    572                         | NumberFormatException
    573                         | XmlPullParserException
    574                         | IOException
    575                         | IndexOutOfBoundsException e) {
    576                     Slog.w(LOG_TAG, "Failed parsing ", e);
    577                 } finally {
    578                     IoUtils.closeQuietly(in);
    579                 }
    580 
    581                 return Collections.emptyList();
    582             }
    583 
    584             private void parseState(XmlPullParser parser, List<PrinterInfo> outPrinters)
    585                     throws IOException, XmlPullParserException {
    586                 parser.next();
    587                 skipEmptyTextTags(parser);
    588                 expect(parser, XmlPullParser.START_TAG, TAG_PRINTERS);
    589                 parser.next();
    590 
    591                 while (parsePrinter(parser, outPrinters)) {
    592                     // Be nice and respond to cancellation
    593                     if (isCancelled()) {
    594                         return;
    595                     }
    596                     parser.next();
    597                 }
    598 
    599                 skipEmptyTextTags(parser);
    600                 expect(parser, XmlPullParser.END_TAG, TAG_PRINTERS);
    601             }
    602 
    603             private boolean parsePrinter(XmlPullParser parser, List<PrinterInfo> outPrinters)
    604                     throws IOException, XmlPullParserException {
    605                 skipEmptyTextTags(parser);
    606                 if (!accept(parser, XmlPullParser.START_TAG, TAG_PRINTER)) {
    607                     return false;
    608                 }
    609 
    610                 String name = parser.getAttributeValue(null, ATTR_NAME);
    611                 String description = parser.getAttributeValue(null, ATTR_DESCRIPTION);
    612                 final int status = Integer.parseInt(parser.getAttributeValue(null, ATTR_STATUS));
    613 
    614                 parser.next();
    615 
    616                 skipEmptyTextTags(parser);
    617                 expect(parser, XmlPullParser.START_TAG, TAG_PRINTER_ID);
    618                 String localId = parser.getAttributeValue(null, ATTR_LOCAL_ID);
    619                 ComponentName service = ComponentName.unflattenFromString(parser.getAttributeValue(
    620                         null, ATTR_SERVICE_NAME));
    621                 PrinterId printerId =  new PrinterId(service, localId);
    622                 parser.next();
    623                 skipEmptyTextTags(parser);
    624                 expect(parser, XmlPullParser.END_TAG, TAG_PRINTER_ID);
    625                 parser.next();
    626 
    627                 PrinterInfo.Builder builder = new PrinterInfo.Builder(printerId, name, status);
    628                 builder.setDescription(description);
    629                 PrinterInfo printer = builder.build();
    630 
    631                 outPrinters.add(printer);
    632 
    633                 if (DEBUG) {
    634                     Log.i(LOG_TAG, "[RESTORED] " + printer);
    635                 }
    636 
    637                 skipEmptyTextTags(parser);
    638                 expect(parser, XmlPullParser.END_TAG, TAG_PRINTER);
    639 
    640                 return true;
    641             }
    642 
    643             private void expect(XmlPullParser parser, int type, String tag)
    644                     throws IOException, XmlPullParserException {
    645                 if (!accept(parser, type, tag)) {
    646                     throw new XmlPullParserException("Exepected event: " + type
    647                             + " and tag: " + tag + " but got event: " + parser.getEventType()
    648                             + " and tag:" + parser.getName());
    649                 }
    650             }
    651 
    652             private void skipEmptyTextTags(XmlPullParser parser)
    653                     throws IOException, XmlPullParserException {
    654                 while (accept(parser, XmlPullParser.TEXT, null)
    655                         && "\n".equals(parser.getText())) {
    656                     parser.next();
    657                 }
    658             }
    659 
    660             private boolean accept(XmlPullParser parser, int type, String tag)
    661                     throws IOException, XmlPullParserException {
    662                 if (parser.getEventType() != type) {
    663                     return false;
    664                 }
    665                 if (tag != null) {
    666                     if (!tag.equals(parser.getName())) {
    667                         return false;
    668                     }
    669                 } else if (parser.getName() != null) {
    670                     return false;
    671                 }
    672                 return true;
    673             }
    674         }
    675 
    676         private final class WriteTask extends AsyncTask<List<PrinterInfo>, Void, Void> {
    677             @Override
    678             protected Void doInBackground(List<PrinterInfo>... printers) {
    679                 doWritePrinterHistory(printers[0]);
    680                 return null;
    681             }
    682 
    683             private void doWritePrinterHistory(List<PrinterInfo> printers) {
    684                 FileOutputStream out = null;
    685                 try {
    686                     out = mStatePersistFile.startWrite();
    687 
    688                     XmlSerializer serializer = new FastXmlSerializer();
    689                     serializer.setOutput(out, "utf-8");
    690                     serializer.startDocument(null, true);
    691                     serializer.startTag(null, TAG_PRINTERS);
    692 
    693                     final int printerCount = printers.size();
    694                     for (int i = 0; i < printerCount; i++) {
    695                         PrinterInfo printer = printers.get(i);
    696 
    697                         serializer.startTag(null, TAG_PRINTER);
    698 
    699                         serializer.attribute(null, ATTR_NAME, printer.getName());
    700                         // Historical printers are always stored as unavailable.
    701                         serializer.attribute(null, ATTR_STATUS, String.valueOf(
    702                                 PrinterInfo.STATUS_UNAVAILABLE));
    703                         String description = printer.getDescription();
    704                         if (description != null) {
    705                             serializer.attribute(null, ATTR_DESCRIPTION, description);
    706                         }
    707 
    708                         PrinterId printerId = printer.getId();
    709                         serializer.startTag(null, TAG_PRINTER_ID);
    710                         serializer.attribute(null, ATTR_LOCAL_ID, printerId.getLocalId());
    711                         serializer.attribute(null, ATTR_SERVICE_NAME, printerId.getServiceName()
    712                                 .flattenToString());
    713                         serializer.endTag(null, TAG_PRINTER_ID);
    714 
    715                         serializer.endTag(null, TAG_PRINTER);
    716 
    717                         if (DEBUG) {
    718                             Log.i(LOG_TAG, "[PERSISTED] " + printer);
    719                         }
    720                     }
    721 
    722                     serializer.endTag(null, TAG_PRINTERS);
    723                     serializer.endDocument();
    724                     mStatePersistFile.finishWrite(out);
    725 
    726                     if (DEBUG) {
    727                         Log.i(LOG_TAG, "[PERSIST END]");
    728                     }
    729                 } catch (IOException ioe) {
    730                     Slog.w(LOG_TAG, "Failed to write printer history, restoring backup.", ioe);
    731                     mStatePersistFile.failWrite(out);
    732                 } finally {
    733                     IoUtils.closeQuietly(out);
    734                 }
    735             }
    736         }
    737     }
    738 }
    739