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