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