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    public FusedPrintersProvider(Context context) {
83        super(context);
84        mPersistenceManager = new PersistenceManager(context);
85    }
86
87    public void addHistoricalPrinter(PrinterInfo printer) {
88        mPersistenceManager.addPrinterAndWritePrinterHistory(printer);
89    }
90
91    private void computeAndDeliverResult(Map<PrinterId, PrinterInfo> discoveredPrinters) {
92        List<PrinterInfo> printers = new ArrayList<PrinterInfo>();
93
94        // Add the updated favorite printers.
95        final int favoritePrinterCount = mFavoritePrinters.size();
96        for (int i = 0; i < favoritePrinterCount; i++) {
97            PrinterInfo favoritePrinter = mFavoritePrinters.get(i);
98            PrinterInfo updatedPrinter = discoveredPrinters.remove(
99                    favoritePrinter.getId());
100            if (updatedPrinter != null) {
101                printers.add(updatedPrinter);
102            } else {
103                printers.add(favoritePrinter);
104            }
105        }
106
107        // Add other updated printers.
108        final int printerCount = mPrinters.size();
109        for (int i = 0; i < printerCount; i++) {
110            PrinterInfo printer = mPrinters.get(i);
111            PrinterInfo updatedPrinter = discoveredPrinters.remove(
112                    printer.getId());
113            if (updatedPrinter != null) {
114                printers.add(updatedPrinter);
115            }
116        }
117
118        // Add the new printers, i.e. what is left.
119        printers.addAll(discoveredPrinters.values());
120
121        // Update the list of printers.
122        mPrinters.clear();
123        mPrinters.addAll(printers);
124
125        if (isStarted()) {
126            // Deliver the printers.
127            deliverResult(printers);
128        }
129    }
130
131    @Override
132    protected void onStartLoading() {
133        if (DEBUG) {
134            Log.i(LOG_TAG, "onStartLoading() " + FusedPrintersProvider.this.hashCode());
135        }
136        // The contract is that if we already have a valid,
137        // result the we have to deliver it immediately.
138        if (!mPrinters.isEmpty()) {
139            deliverResult(new ArrayList<PrinterInfo>(mPrinters));
140        }
141        // Always load the data to ensure discovery period is
142        // started and to make sure obsolete printers are updated.
143        onForceLoad();
144    }
145
146    @Override
147    protected void onStopLoading() {
148        if (DEBUG) {
149            Log.i(LOG_TAG, "onStopLoading() " + FusedPrintersProvider.this.hashCode());
150        }
151        onCancelLoad();
152    }
153
154    @Override
155    protected void onForceLoad() {
156        if (DEBUG) {
157            Log.i(LOG_TAG, "onForceLoad() " + FusedPrintersProvider.this.hashCode());
158        }
159        loadInternal();
160    }
161
162    private void loadInternal() {
163        if (mDiscoverySession == null) {
164            PrintManager printManager = (PrintManager) getContext()
165                    .getSystemService(Context.PRINT_SERVICE);
166            mDiscoverySession = printManager.createPrinterDiscoverySession();
167            mPersistenceManager.readPrinterHistory();
168        }
169        if (mPersistenceManager.isReadHistoryCompleted()
170                && !mDiscoverySession.isPrinterDiscoveryStarted()) {
171            mDiscoverySession.setOnPrintersChangeListener(new OnPrintersChangeListener() {
172                @Override
173                public void onPrintersChanged() {
174                    if (DEBUG) {
175                        Log.i(LOG_TAG, "onPrintersChanged() count:"
176                                + mDiscoverySession.getPrinters().size()
177                                + " " + FusedPrintersProvider.this.hashCode());
178                    }
179                    updatePrinters(mDiscoverySession.getPrinters());
180                }
181            });
182            final int favoriteCount = mFavoritePrinters.size();
183            List<PrinterId> printerIds = new ArrayList<PrinterId>(favoriteCount);
184            for (int i = 0; i < favoriteCount; i++) {
185                printerIds.add(mFavoritePrinters.get(i).getId());
186            }
187            mDiscoverySession.startPrinterDisovery(printerIds);
188            List<PrinterInfo> printers = mDiscoverySession.getPrinters();
189            if (!printers.isEmpty()) {
190                updatePrinters(printers);
191            }
192        }
193    }
194
195    private void updatePrinters(List<PrinterInfo> printers) {
196        if (mPrinters.equals(printers)) {
197            return;
198        }
199        ArrayMap<PrinterId, PrinterInfo> printersMap =
200                new ArrayMap<PrinterId, PrinterInfo>();
201        final int printerCount = printers.size();
202        for (int i = 0; i < printerCount; i++) {
203            PrinterInfo printer = printers.get(i);
204            printersMap.put(printer.getId(), printer);
205        }
206        computeAndDeliverResult(printersMap);
207    }
208
209    @Override
210    protected boolean onCancelLoad() {
211        if (DEBUG) {
212            Log.i(LOG_TAG, "onCancelLoad() " + FusedPrintersProvider.this.hashCode());
213        }
214        return cancelInternal();
215    }
216
217    private boolean cancelInternal() {
218        if (mDiscoverySession != null
219                && mDiscoverySession.isPrinterDiscoveryStarted()) {
220            if (mTrackedPrinter != null) {
221                mDiscoverySession.stopPrinterStateTracking(mTrackedPrinter);
222                mTrackedPrinter = null;
223            }
224            mDiscoverySession.stopPrinterDiscovery();
225            return true;
226        } else if (mPersistenceManager.isReadHistoryInProgress()) {
227            return mPersistenceManager.stopReadPrinterHistory();
228        }
229        return false;
230    }
231
232    @Override
233    protected void onReset() {
234        if (DEBUG) {
235            Log.i(LOG_TAG, "onReset() " + FusedPrintersProvider.this.hashCode());
236        }
237        onStopLoading();
238        mPrinters.clear();
239        if (mDiscoverySession != null) {
240            mDiscoverySession.destroy();
241            mDiscoverySession = null;
242        }
243    }
244
245    @Override
246    protected void onAbandon() {
247        if (DEBUG) {
248            Log.i(LOG_TAG, "onAbandon() " + FusedPrintersProvider.this.hashCode());
249        }
250        onStopLoading();
251    }
252
253    public void setTrackedPrinter(PrinterId printerId) {
254        if (isStarted() && mDiscoverySession != null
255                && mDiscoverySession.isPrinterDiscoveryStarted()) {
256            if (mTrackedPrinter != null) {
257                if (mTrackedPrinter.equals(printerId)) {
258                    return;
259                }
260                mDiscoverySession.stopPrinterStateTracking(mTrackedPrinter);
261            }
262            mTrackedPrinter = printerId;
263            mDiscoverySession.startPrinterStateTracking(printerId);
264        }
265    }
266
267    private final class PersistenceManager {
268        private static final String PERSIST_FILE_NAME = "printer_history.xml";
269
270        private static final String TAG_PRINTERS = "printers";
271
272        private static final String TAG_PRINTER = "printer";
273        private static final String TAG_PRINTER_ID = "printerId";
274
275        private static final String ATTR_LOCAL_ID = "localId";
276        private static final String ATTR_SERVICE_NAME = "serviceName";
277
278        private static final String ATTR_NAME = "name";
279        private static final String ATTR_DESCRIPTION = "description";
280        private static final String ATTR_STATUS = "status";
281
282        private final AtomicFile mStatePersistFile;
283
284        private List<PrinterInfo> mHistoricalPrinters;
285
286        private boolean mReadHistoryCompleted;
287        private boolean mReadHistoryInProgress;
288
289        private ReadTask mReadTask;
290
291        private PersistenceManager(Context context) {
292            mStatePersistFile = new AtomicFile(new File(context.getFilesDir(),
293                    PERSIST_FILE_NAME));
294        }
295
296        public boolean isReadHistoryInProgress() {
297            return mReadHistoryInProgress;
298        }
299
300        public boolean isReadHistoryCompleted() {
301            return mReadHistoryCompleted;
302        }
303
304        public boolean stopReadPrinterHistory() {
305            final boolean cancelled = mReadTask.cancel(true);
306            mReadTask = null;
307            return cancelled;
308        }
309
310        public void readPrinterHistory() {
311            if (DEBUG) {
312                Log.i(LOG_TAG, "read history started "
313                        + FusedPrintersProvider.this.hashCode());
314            }
315            mReadHistoryInProgress = true;
316            mReadTask = new ReadTask();
317            mReadTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
318        }
319
320        @SuppressWarnings("unchecked")
321        public void addPrinterAndWritePrinterHistory(PrinterInfo printer) {
322            if (mHistoricalPrinters.size() >= MAX_HISTORY_LENGTH) {
323                mHistoricalPrinters.remove(0);
324            }
325            mHistoricalPrinters.add(printer);
326            new WriteTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,
327                    new ArrayList<PrinterInfo>(mHistoricalPrinters));
328        }
329
330        private List<PrinterInfo> computeFavoritePrinters(List<PrinterInfo> printers) {
331            Map<PrinterId, PrinterRecord> recordMap =
332                    new ArrayMap<PrinterId, PrinterRecord>();
333
334            // Recompute the weights.
335            float currentWeight = 1.0f;
336            final int printerCount = printers.size();
337            for (int i = printerCount - 1; i >= 0; i--) {
338                PrinterInfo printer = printers.get(i);
339                // Aggregate weight for the same printer
340                PrinterRecord record = recordMap.get(printer.getId());
341                if (record == null) {
342                    record = new PrinterRecord(printer);
343                    recordMap.put(printer.getId(), record);
344                }
345                record.weight += currentWeight;
346                currentWeight *= WEIGHT_DECAY_COEFFICIENT;
347            }
348
349            // Soft the favorite printers.
350            List<PrinterRecord> favoriteRecords = new ArrayList<PrinterRecord>(
351                    recordMap.values());
352            Collections.sort(favoriteRecords);
353
354            // Write the favorites to the output.
355            final int favoriteCount = Math.min(favoriteRecords.size(),
356                    MAX_FAVORITE_PRINTER_COUNT);
357            List<PrinterInfo> favoritePrinters = new ArrayList<PrinterInfo>(favoriteCount);
358            for (int i = 0; i < favoriteCount; i++) {
359                PrinterInfo printer = favoriteRecords.get(i).printer;
360                favoritePrinters.add(printer);
361            }
362
363            return favoritePrinters;
364        }
365
366        private final class PrinterRecord implements Comparable<PrinterRecord> {
367            public final PrinterInfo printer;
368            public float weight;
369
370            public PrinterRecord(PrinterInfo printer) {
371                this.printer = printer;
372            }
373
374            @Override
375            public int compareTo(PrinterRecord another) {
376                return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
377            }
378        }
379
380        private final class ReadTask extends AsyncTask<Void, Void, List<PrinterInfo>> {
381            @Override
382            protected List<PrinterInfo> doInBackground(Void... args) {
383               return doReadPrinterHistory();
384            }
385
386            @Override
387            protected void onPostExecute(List<PrinterInfo> printers) {
388                if (DEBUG) {
389                    Log.i(LOG_TAG, "read history completed "
390                            + FusedPrintersProvider.this.hashCode());
391                }
392
393                // Ignore printer records whose target services are not enabled.
394                PrintManager printManager = (PrintManager) getContext()
395                        .getSystemService(Context.PRINT_SERVICE);
396                List<PrintServiceInfo> services = printManager
397                        .getEnabledPrintServices();
398
399                Set<ComponentName> enabledComponents = new ArraySet<ComponentName>();
400                final int installedServiceCount = services.size();
401                for (int i = 0; i < installedServiceCount; i++) {
402                    ServiceInfo serviceInfo = services.get(i).getResolveInfo().serviceInfo;
403                    ComponentName componentName = new ComponentName(
404                            serviceInfo.packageName, serviceInfo.name);
405                    enabledComponents.add(componentName);
406                }
407
408                final int printerCount = printers.size();
409                for (int i = printerCount - 1; i >= 0; i--) {
410                    ComponentName printerServiceName = printers.get(i).getId().getServiceName();
411                    if (!enabledComponents.contains(printerServiceName)) {
412                        printers.remove(i);
413                    }
414                }
415
416                // Store the filtered list.
417                mHistoricalPrinters = printers;
418
419                // Compute the favorite printers.
420                mFavoritePrinters.clear();
421                mFavoritePrinters.addAll(computeFavoritePrinters(mHistoricalPrinters));
422
423                mReadHistoryInProgress = false;
424                mReadHistoryCompleted = true;
425
426                // Deliver the favorites.
427                Map<PrinterId, PrinterInfo> discoveredPrinters = Collections.emptyMap();
428                computeAndDeliverResult(discoveredPrinters);
429
430                // Start loading the available printers.
431                loadInternal();
432
433                // We are done.
434                mReadTask = null;
435            }
436
437            private List<PrinterInfo> doReadPrinterHistory() {
438                FileInputStream in = null;
439                try {
440                    in = mStatePersistFile.openRead();
441                } catch (FileNotFoundException fnfe) {
442                    if (DEBUG) {
443                        Log.i(LOG_TAG, "No existing printer history "
444                                + FusedPrintersProvider.this.hashCode());
445                    }
446                    return new ArrayList<PrinterInfo>();
447                }
448                try {
449                    List<PrinterInfo> printers = new ArrayList<PrinterInfo>();
450                    XmlPullParser parser = Xml.newPullParser();
451                    parser.setInput(in, null);
452                    parseState(parser, printers);
453                    return printers;
454                } catch (IllegalStateException ise) {
455                    Slog.w(LOG_TAG, "Failed parsing ", ise);
456                } catch (NullPointerException npe) {
457                    Slog.w(LOG_TAG, "Failed parsing ", npe);
458                } catch (NumberFormatException nfe) {
459                    Slog.w(LOG_TAG, "Failed parsing ", nfe);
460                } catch (XmlPullParserException xppe) {
461                    Slog.w(LOG_TAG, "Failed parsing ", xppe);
462                } catch (IOException ioe) {
463                    Slog.w(LOG_TAG, "Failed parsing ", ioe);
464                } catch (IndexOutOfBoundsException iobe) {
465                    Slog.w(LOG_TAG, "Failed parsing ", iobe);
466                } finally {
467                    IoUtils.closeQuietly(in);
468                }
469
470                return Collections.emptyList();
471            }
472
473            private void parseState(XmlPullParser parser, List<PrinterInfo> outPrinters)
474                    throws IOException, XmlPullParserException {
475                parser.next();
476                skipEmptyTextTags(parser);
477                expect(parser, XmlPullParser.START_TAG, TAG_PRINTERS);
478                parser.next();
479
480                while (parsePrinter(parser, outPrinters)) {
481                    // Be nice and respond to cancellation
482                    if (isCancelled()) {
483                        return;
484                    }
485                    parser.next();
486                }
487
488                skipEmptyTextTags(parser);
489                expect(parser, XmlPullParser.END_TAG, TAG_PRINTERS);
490            }
491
492            private boolean parsePrinter(XmlPullParser parser, List<PrinterInfo> outPrinters)
493                    throws IOException, XmlPullParserException {
494                skipEmptyTextTags(parser);
495                if (!accept(parser, XmlPullParser.START_TAG, TAG_PRINTER)) {
496                    return false;
497                }
498
499                String name = parser.getAttributeValue(null, ATTR_NAME);
500                String description = parser.getAttributeValue(null, ATTR_DESCRIPTION);
501                final int status = Integer.parseInt(parser.getAttributeValue(null, ATTR_STATUS));
502
503                parser.next();
504
505                skipEmptyTextTags(parser);
506                expect(parser, XmlPullParser.START_TAG, TAG_PRINTER_ID);
507                String localId = parser.getAttributeValue(null, ATTR_LOCAL_ID);
508                ComponentName service = ComponentName.unflattenFromString(parser.getAttributeValue(
509                        null, ATTR_SERVICE_NAME));
510                PrinterId printerId =  new PrinterId(service, localId);
511                parser.next();
512                skipEmptyTextTags(parser);
513                expect(parser, XmlPullParser.END_TAG, TAG_PRINTER_ID);
514                parser.next();
515
516                PrinterInfo.Builder builder = new PrinterInfo.Builder(printerId, name, status);
517                builder.setDescription(description);
518                PrinterInfo printer = builder.build();
519
520                outPrinters.add(printer);
521
522                if (DEBUG) {
523                    Log.i(LOG_TAG, "[RESTORED] " + printer);
524                }
525
526                skipEmptyTextTags(parser);
527                expect(parser, XmlPullParser.END_TAG, TAG_PRINTER);
528
529                return true;
530            }
531
532            private void expect(XmlPullParser parser, int type, String tag)
533                    throws IOException, XmlPullParserException {
534                if (!accept(parser, type, tag)) {
535                    throw new XmlPullParserException("Exepected event: " + type
536                            + " and tag: " + tag + " but got event: " + parser.getEventType()
537                            + " and tag:" + parser.getName());
538                }
539            }
540
541            private void skipEmptyTextTags(XmlPullParser parser)
542                    throws IOException, XmlPullParserException {
543                while (accept(parser, XmlPullParser.TEXT, null)
544                        && "\n".equals(parser.getText())) {
545                    parser.next();
546                }
547            }
548
549            private boolean accept(XmlPullParser parser, int type, String tag)
550                    throws IOException, XmlPullParserException {
551                if (parser.getEventType() != type) {
552                    return false;
553                }
554                if (tag != null) {
555                    if (!tag.equals(parser.getName())) {
556                        return false;
557                    }
558                } else if (parser.getName() != null) {
559                    return false;
560                }
561                return true;
562            }
563        };
564
565        private final class WriteTask extends AsyncTask<List<PrinterInfo>, Void, Void> {
566            @Override
567            protected Void doInBackground(List<PrinterInfo>... printers) {
568                doWritePrinterHistory(printers[0]);
569                return null;
570            }
571
572            private void doWritePrinterHistory(List<PrinterInfo> printers) {
573                FileOutputStream out = null;
574                try {
575                    out = mStatePersistFile.startWrite();
576
577                    XmlSerializer serializer = new FastXmlSerializer();
578                    serializer.setOutput(out, "utf-8");
579                    serializer.startDocument(null, true);
580                    serializer.startTag(null, TAG_PRINTERS);
581
582                    final int printerCount = printers.size();
583                    for (int i = 0; i < printerCount; i++) {
584                        PrinterInfo printer = printers.get(i);
585
586                        serializer.startTag(null, TAG_PRINTER);
587
588                        serializer.attribute(null, ATTR_NAME, printer.getName());
589                        // Historical printers are always stored as unavailable.
590                        serializer.attribute(null, ATTR_STATUS, String.valueOf(
591                                PrinterInfo.STATUS_UNAVAILABLE));
592                        String description = printer.getDescription();
593                        if (description != null) {
594                            serializer.attribute(null, ATTR_DESCRIPTION, description);
595                        }
596
597                        PrinterId printerId = printer.getId();
598                        serializer.startTag(null, TAG_PRINTER_ID);
599                        serializer.attribute(null, ATTR_LOCAL_ID, printerId.getLocalId());
600                        serializer.attribute(null, ATTR_SERVICE_NAME, printerId.getServiceName()
601                                .flattenToString());
602                        serializer.endTag(null, TAG_PRINTER_ID);
603
604                        serializer.endTag(null, TAG_PRINTER);
605
606                        if (DEBUG) {
607                            Log.i(LOG_TAG, "[PERSISTED] " + printer);
608                        }
609                    }
610
611                    serializer.endTag(null, TAG_PRINTERS);
612                    serializer.endDocument();
613                    mStatePersistFile.finishWrite(out);
614
615                    if (DEBUG) {
616                        Log.i(LOG_TAG, "[PERSIST END]");
617                    }
618                } catch (IOException ioe) {
619                    Slog.w(LOG_TAG, "Failed to write printer history, restoring backup.", ioe);
620                    mStatePersistFile.failWrite(out);
621                } finally {
622                    IoUtils.closeQuietly(out);
623                }
624            }
625        };
626    }
627}
628