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