FusedPrintersProvider.java revision a798c0a984f29f7180883a61839f68d2cbf0c6ce
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 void setTrackedPrinter(PrinterId printerId) {
275        if (isStarted() && mDiscoverySession != null
276                && mDiscoverySession.isPrinterDiscoveryStarted()) {
277            if (mTrackedPrinter != null) {
278                if (mTrackedPrinter.equals(printerId)) {
279                    return;
280                }
281                mDiscoverySession.stopPrinterStateTracking(mTrackedPrinter);
282            }
283            mTrackedPrinter = printerId;
284            if (printerId != null) {
285                mDiscoverySession.startPrinterStateTracking(printerId);
286            }
287        }
288    }
289
290    public boolean isFavoritePrinter(PrinterId printerId) {
291        final int printerCount = mFavoritePrinters.size();
292        for (int i = 0; i < printerCount; i++) {
293            PrinterInfo favoritePritner = mFavoritePrinters.get(i);
294            if (favoritePritner.getId().equals(printerId)) {
295                return true;
296            }
297        }
298        return false;
299    }
300
301    public void forgetFavoritePrinter(PrinterId printerId) {
302        List<PrinterInfo> newFavoritePrinters = null;
303
304        // Remove the printer from the favorites.
305        final int favoritePrinterCount = mFavoritePrinters.size();
306        for (int i = 0; i < favoritePrinterCount; i++) {
307            PrinterInfo favoritePrinter = mFavoritePrinters.get(i);
308            if (favoritePrinter.getId().equals(printerId)) {
309                newFavoritePrinters = new ArrayList<PrinterInfo>();
310                newFavoritePrinters.addAll(mPrinters);
311                newFavoritePrinters.remove(i);
312                break;
313            }
314        }
315
316        // If we removed a favorite printer, we have work to do.
317        if (newFavoritePrinters != null) {
318            // Remove the printer from history and persist the latter.
319            mPersistenceManager.removeHistoricalPrinterAndWritePrinterHistory(printerId);
320
321            // Recompute and deliver the printers.
322            updatePrinters(mDiscoverySession.getPrinters(), newFavoritePrinters);
323        }
324    }
325
326    private final class PersistenceManager {
327        private static final String PERSIST_FILE_NAME = "printer_history.xml";
328
329        private static final String TAG_PRINTERS = "printers";
330
331        private static final String TAG_PRINTER = "printer";
332        private static final String TAG_PRINTER_ID = "printerId";
333
334        private static final String ATTR_LOCAL_ID = "localId";
335        private static final String ATTR_SERVICE_NAME = "serviceName";
336
337        private static final String ATTR_NAME = "name";
338        private static final String ATTR_DESCRIPTION = "description";
339        private static final String ATTR_STATUS = "status";
340
341        private final AtomicFile mStatePersistFile;
342
343        private List<PrinterInfo> mHistoricalPrinters = new ArrayList<PrinterInfo>();
344
345        private boolean mReadHistoryCompleted;
346        private boolean mReadHistoryInProgress;
347
348        private ReadTask mReadTask;
349
350        private volatile long mLastReadHistoryTimestamp;
351
352        private PersistenceManager(Context context) {
353            mStatePersistFile = new AtomicFile(new File(context.getFilesDir(),
354                    PERSIST_FILE_NAME));
355        }
356
357        public boolean isReadHistoryInProgress() {
358            return mReadHistoryInProgress;
359        }
360
361        public boolean isReadHistoryCompleted() {
362            return mReadHistoryCompleted;
363        }
364
365        public boolean stopReadPrinterHistory() {
366            final boolean cancelled = mReadTask.cancel(true);
367            mReadTask = null;
368            return cancelled;
369        }
370
371        public void readPrinterHistory() {
372            if (DEBUG) {
373                Log.i(LOG_TAG, "read history started "
374                        + FusedPrintersProvider.this.hashCode());
375            }
376            mReadHistoryInProgress = true;
377            mReadTask = new ReadTask();
378            mReadTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
379        }
380
381        @SuppressWarnings("unchecked")
382        public void addPrinterAndWritePrinterHistory(PrinterInfo printer) {
383            if (mHistoricalPrinters.size() >= MAX_HISTORY_LENGTH) {
384                mHistoricalPrinters.remove(0);
385            }
386            mHistoricalPrinters.add(printer);
387            new WriteTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,
388                    new ArrayList<PrinterInfo>(mHistoricalPrinters));
389        }
390
391        @SuppressWarnings("unchecked")
392        public void removeHistoricalPrinterAndWritePrinterHistory(PrinterId printerId) {
393            boolean writeHistory = false;
394            final int printerCount = mHistoricalPrinters.size();
395            for (int i = printerCount - 1; i >= 0; i--) {
396                PrinterInfo historicalPrinter = mHistoricalPrinters.get(i);
397                if (historicalPrinter.getId().equals(printerId)) {
398                    mHistoricalPrinters.remove(i);
399                    writeHistory = true;
400                }
401            }
402            if (writeHistory) {
403                new WriteTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,
404                        new ArrayList<PrinterInfo>(mHistoricalPrinters));
405            }
406        }
407
408        public boolean isHistoryChanged() {
409            return mLastReadHistoryTimestamp != mStatePersistFile.getBaseFile().lastModified();
410        }
411
412        private List<PrinterInfo> computeFavoritePrinters(List<PrinterInfo> printers) {
413            Map<PrinterId, PrinterRecord> recordMap =
414                    new ArrayMap<PrinterId, PrinterRecord>();
415
416            // Recompute the weights.
417            float currentWeight = 1.0f;
418            final int printerCount = printers.size();
419            for (int i = printerCount - 1; i >= 0; i--) {
420                PrinterInfo printer = printers.get(i);
421                // Aggregate weight for the same printer
422                PrinterRecord record = recordMap.get(printer.getId());
423                if (record == null) {
424                    record = new PrinterRecord(printer);
425                    recordMap.put(printer.getId(), record);
426                }
427                record.weight += currentWeight;
428                currentWeight *= WEIGHT_DECAY_COEFFICIENT;
429            }
430
431            // Soft the favorite printers.
432            List<PrinterRecord> favoriteRecords = new ArrayList<PrinterRecord>(
433                    recordMap.values());
434            Collections.sort(favoriteRecords);
435
436            // Write the favorites to the output.
437            final int favoriteCount = Math.min(favoriteRecords.size(),
438                    MAX_FAVORITE_PRINTER_COUNT);
439            List<PrinterInfo> favoritePrinters = new ArrayList<PrinterInfo>(favoriteCount);
440            for (int i = 0; i < favoriteCount; i++) {
441                PrinterInfo printer = favoriteRecords.get(i).printer;
442                favoritePrinters.add(printer);
443            }
444
445            return favoritePrinters;
446        }
447
448        private final class PrinterRecord implements Comparable<PrinterRecord> {
449            public final PrinterInfo printer;
450            public float weight;
451
452            public PrinterRecord(PrinterInfo printer) {
453                this.printer = printer;
454            }
455
456            @Override
457            public int compareTo(PrinterRecord another) {
458                return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
459            }
460        }
461
462        private final class ReadTask extends AsyncTask<Void, Void, List<PrinterInfo>> {
463            @Override
464            protected List<PrinterInfo> doInBackground(Void... args) {
465               return doReadPrinterHistory();
466            }
467
468            @Override
469            protected void onPostExecute(List<PrinterInfo> printers) {
470                if (DEBUG) {
471                    Log.i(LOG_TAG, "read history completed "
472                            + FusedPrintersProvider.this.hashCode());
473                }
474
475                // Ignore printer records whose target services are not enabled.
476                PrintManager printManager = (PrintManager) getContext()
477                        .getSystemService(Context.PRINT_SERVICE);
478                List<PrintServiceInfo> services = printManager
479                        .getEnabledPrintServices();
480
481                Set<ComponentName> enabledComponents = new ArraySet<ComponentName>();
482                final int installedServiceCount = services.size();
483                for (int i = 0; i < installedServiceCount; i++) {
484                    ServiceInfo serviceInfo = services.get(i).getResolveInfo().serviceInfo;
485                    ComponentName componentName = new ComponentName(
486                            serviceInfo.packageName, serviceInfo.name);
487                    enabledComponents.add(componentName);
488                }
489
490                final int printerCount = printers.size();
491                for (int i = printerCount - 1; i >= 0; i--) {
492                    ComponentName printerServiceName = printers.get(i).getId().getServiceName();
493                    if (!enabledComponents.contains(printerServiceName)) {
494                        printers.remove(i);
495                    }
496                }
497
498                // Store the filtered list.
499                mHistoricalPrinters = printers;
500
501                // Compute the favorite printers.
502                mFavoritePrinters.clear();
503                mFavoritePrinters.addAll(computeFavoritePrinters(mHistoricalPrinters));
504
505                mReadHistoryInProgress = false;
506                mReadHistoryCompleted = true;
507
508                // Deliver the printers.
509                updatePrinters(mDiscoverySession.getPrinters(), mHistoricalPrinters);
510
511                // Loading the available printers if needed.
512                loadInternal();
513
514                // We are done.
515                mReadTask = null;
516            }
517
518            private List<PrinterInfo> doReadPrinterHistory() {
519                final FileInputStream in;
520                try {
521                    in = mStatePersistFile.openRead();
522                } catch (FileNotFoundException fnfe) {
523                    if (DEBUG) {
524                        Log.i(LOG_TAG, "No existing printer history "
525                                + FusedPrintersProvider.this.hashCode());
526                    }
527                    return new ArrayList<PrinterInfo>();
528                }
529                try {
530                    List<PrinterInfo> printers = new ArrayList<PrinterInfo>();
531                    XmlPullParser parser = Xml.newPullParser();
532                    parser.setInput(in, null);
533                    parseState(parser, printers);
534                    // Take a note which version of the history was read.
535                    mLastReadHistoryTimestamp = mStatePersistFile.getBaseFile().lastModified();
536                    return printers;
537                } catch (IllegalStateException ise) {
538                    Slog.w(LOG_TAG, "Failed parsing ", ise);
539                } catch (NullPointerException npe) {
540                    Slog.w(LOG_TAG, "Failed parsing ", npe);
541                } catch (NumberFormatException nfe) {
542                    Slog.w(LOG_TAG, "Failed parsing ", nfe);
543                } catch (XmlPullParserException xppe) {
544                    Slog.w(LOG_TAG, "Failed parsing ", xppe);
545                } catch (IOException ioe) {
546                    Slog.w(LOG_TAG, "Failed parsing ", ioe);
547                } catch (IndexOutOfBoundsException iobe) {
548                    Slog.w(LOG_TAG, "Failed parsing ", iobe);
549                } finally {
550                    IoUtils.closeQuietly(in);
551                }
552
553                return Collections.emptyList();
554            }
555
556            private void parseState(XmlPullParser parser, List<PrinterInfo> outPrinters)
557                    throws IOException, XmlPullParserException {
558                parser.next();
559                skipEmptyTextTags(parser);
560                expect(parser, XmlPullParser.START_TAG, TAG_PRINTERS);
561                parser.next();
562
563                while (parsePrinter(parser, outPrinters)) {
564                    // Be nice and respond to cancellation
565                    if (isCancelled()) {
566                        return;
567                    }
568                    parser.next();
569                }
570
571                skipEmptyTextTags(parser);
572                expect(parser, XmlPullParser.END_TAG, TAG_PRINTERS);
573            }
574
575            private boolean parsePrinter(XmlPullParser parser, List<PrinterInfo> outPrinters)
576                    throws IOException, XmlPullParserException {
577                skipEmptyTextTags(parser);
578                if (!accept(parser, XmlPullParser.START_TAG, TAG_PRINTER)) {
579                    return false;
580                }
581
582                String name = parser.getAttributeValue(null, ATTR_NAME);
583                String description = parser.getAttributeValue(null, ATTR_DESCRIPTION);
584                final int status = Integer.parseInt(parser.getAttributeValue(null, ATTR_STATUS));
585
586                parser.next();
587
588                skipEmptyTextTags(parser);
589                expect(parser, XmlPullParser.START_TAG, TAG_PRINTER_ID);
590                String localId = parser.getAttributeValue(null, ATTR_LOCAL_ID);
591                ComponentName service = ComponentName.unflattenFromString(parser.getAttributeValue(
592                        null, ATTR_SERVICE_NAME));
593                PrinterId printerId =  new PrinterId(service, localId);
594                parser.next();
595                skipEmptyTextTags(parser);
596                expect(parser, XmlPullParser.END_TAG, TAG_PRINTER_ID);
597                parser.next();
598
599                PrinterInfo.Builder builder = new PrinterInfo.Builder(printerId, name, status);
600                builder.setDescription(description);
601                PrinterInfo printer = builder.build();
602
603                outPrinters.add(printer);
604
605                if (DEBUG) {
606                    Log.i(LOG_TAG, "[RESTORED] " + printer);
607                }
608
609                skipEmptyTextTags(parser);
610                expect(parser, XmlPullParser.END_TAG, TAG_PRINTER);
611
612                return true;
613            }
614
615            private void expect(XmlPullParser parser, int type, String tag)
616                    throws IOException, XmlPullParserException {
617                if (!accept(parser, type, tag)) {
618                    throw new XmlPullParserException("Exepected event: " + type
619                            + " and tag: " + tag + " but got event: " + parser.getEventType()
620                            + " and tag:" + parser.getName());
621                }
622            }
623
624            private void skipEmptyTextTags(XmlPullParser parser)
625                    throws IOException, XmlPullParserException {
626                while (accept(parser, XmlPullParser.TEXT, null)
627                        && "\n".equals(parser.getText())) {
628                    parser.next();
629                }
630            }
631
632            private boolean accept(XmlPullParser parser, int type, String tag)
633                    throws IOException, XmlPullParserException {
634                if (parser.getEventType() != type) {
635                    return false;
636                }
637                if (tag != null) {
638                    if (!tag.equals(parser.getName())) {
639                        return false;
640                    }
641                } else if (parser.getName() != null) {
642                    return false;
643                }
644                return true;
645            }
646        }
647
648        private final class WriteTask extends AsyncTask<List<PrinterInfo>, Void, Void> {
649            @Override
650            protected Void doInBackground(List<PrinterInfo>... printers) {
651                doWritePrinterHistory(printers[0]);
652                return null;
653            }
654
655            private void doWritePrinterHistory(List<PrinterInfo> printers) {
656                FileOutputStream out = null;
657                try {
658                    out = mStatePersistFile.startWrite();
659
660                    XmlSerializer serializer = new FastXmlSerializer();
661                    serializer.setOutput(out, "utf-8");
662                    serializer.startDocument(null, true);
663                    serializer.startTag(null, TAG_PRINTERS);
664
665                    final int printerCount = printers.size();
666                    for (int i = 0; i < printerCount; i++) {
667                        PrinterInfo printer = printers.get(i);
668
669                        serializer.startTag(null, TAG_PRINTER);
670
671                        serializer.attribute(null, ATTR_NAME, printer.getName());
672                        // Historical printers are always stored as unavailable.
673                        serializer.attribute(null, ATTR_STATUS, String.valueOf(
674                                PrinterInfo.STATUS_UNAVAILABLE));
675                        String description = printer.getDescription();
676                        if (description != null) {
677                            serializer.attribute(null, ATTR_DESCRIPTION, description);
678                        }
679
680                        PrinterId printerId = printer.getId();
681                        serializer.startTag(null, TAG_PRINTER_ID);
682                        serializer.attribute(null, ATTR_LOCAL_ID, printerId.getLocalId());
683                        serializer.attribute(null, ATTR_SERVICE_NAME, printerId.getServiceName()
684                                .flattenToString());
685                        serializer.endTag(null, TAG_PRINTER_ID);
686
687                        serializer.endTag(null, TAG_PRINTER);
688
689                        if (DEBUG) {
690                            Log.i(LOG_TAG, "[PERSISTED] " + printer);
691                        }
692                    }
693
694                    serializer.endTag(null, TAG_PRINTERS);
695                    serializer.endDocument();
696                    mStatePersistFile.finishWrite(out);
697
698                    if (DEBUG) {
699                        Log.i(LOG_TAG, "[PERSIST END]");
700                    }
701                } catch (IOException ioe) {
702                    Slog.w(LOG_TAG, "Failed to write printer history, restoring backup.", ioe);
703                    mStatePersistFile.failWrite(out);
704                } finally {
705                    IoUtils.closeQuietly(out);
706                }
707            }
708        }
709    }
710}
711