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