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