Home | History | Annotate | Download | only in ui
      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 
     17 package com.android.printspooler.ui;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.app.Activity;
     22 import android.app.LoaderManager;
     23 import android.content.ComponentName;
     24 import android.content.Context;
     25 import android.content.Loader;
     26 import android.content.pm.ServiceInfo;
     27 import android.location.Criteria;
     28 import android.location.Location;
     29 import android.location.LocationListener;
     30 import android.location.LocationManager;
     31 import android.location.LocationRequest;
     32 import android.os.AsyncTask;
     33 import android.os.Bundle;
     34 import android.os.Handler;
     35 import android.os.Looper;
     36 import android.os.SystemClock;
     37 import android.print.PrintManager;
     38 import android.print.PrintServicesLoader;
     39 import android.print.PrinterDiscoverySession;
     40 import android.print.PrinterDiscoverySession.OnPrintersChangeListener;
     41 import android.print.PrinterId;
     42 import android.print.PrinterInfo;
     43 import android.printservice.PrintServiceInfo;
     44 import android.util.ArrayMap;
     45 import android.util.ArraySet;
     46 import android.util.AtomicFile;
     47 import android.util.Log;
     48 import android.util.Pair;
     49 import android.util.Slog;
     50 import android.util.Xml;
     51 
     52 import com.android.internal.util.FastXmlSerializer;
     53 
     54 import libcore.io.IoUtils;
     55 
     56 import org.xmlpull.v1.XmlPullParser;
     57 import org.xmlpull.v1.XmlPullParserException;
     58 import org.xmlpull.v1.XmlSerializer;
     59 
     60 import java.io.File;
     61 import java.io.FileInputStream;
     62 import java.io.FileNotFoundException;
     63 import java.io.FileOutputStream;
     64 import java.io.IOException;
     65 import java.nio.charset.StandardCharsets;
     66 import java.util.ArrayList;
     67 import java.util.Collections;
     68 import java.util.HashSet;
     69 import java.util.LinkedHashMap;
     70 import java.util.List;
     71 import java.util.Map;
     72 import java.util.Objects;
     73 import java.util.Set;
     74 
     75 /**
     76  * This class is responsible for loading printers by doing discovery
     77  * and merging the discovered printers with the previously used ones.
     78  */
     79 public final class FusedPrintersProvider extends Loader<List<PrinterInfo>>
     80         implements LocationListener {
     81     private static final String LOG_TAG = "FusedPrintersProvider";
     82 
     83     private static final boolean DEBUG = false;
     84 
     85     private static final double WEIGHT_DECAY_COEFFICIENT = 0.95f;
     86     private static final int MAX_HISTORY_LENGTH = 50;
     87 
     88     private static final int MAX_FAVORITE_PRINTER_COUNT = 4;
     89 
     90     /** Interval of location updated in ms */
     91     private static final int LOCATION_UPDATE_MS = 30 * 1000;
     92 
     93     /** Maximum acceptable age of the location in ms */
     94     private static final int MAX_LOCATION_AGE_MS = 10 * 60 * 1000;
     95 
     96     /** The worst accuracy that is considered usable in m */
     97     private static final int MIN_LOCATION_ACCURACY = 50;
     98 
     99     /** Maximum distance where a printer is still considered "near" */
    100     private static final int MAX_PRINTER_DISTANCE = MIN_LOCATION_ACCURACY * 2;
    101 
    102     private final List<PrinterInfo> mPrinters =
    103             new ArrayList<>();
    104 
    105     private final List<Pair<PrinterInfo, Location>> mFavoritePrinters =
    106             new ArrayList<>();
    107 
    108     private final PersistenceManager mPersistenceManager;
    109 
    110     private PrinterDiscoverySession mDiscoverySession;
    111 
    112     private PrinterId mTrackedPrinter;
    113 
    114     private boolean mPrintersUpdatedBefore;
    115 
    116     /** Last known location, can be null or out of date */
    117     private final Object mLocationLock;
    118     private Location mLocation;
    119 
    120     /** Location used when the printers were updated the last time */
    121     private Location mLocationOfLastPrinterUpdate;
    122 
    123     /** Reference to the system's location manager */
    124     private final LocationManager mLocationManager;
    125 
    126     /**
    127      * Get a reference to the current location.
    128      */
    129     private Location getCurrentLocation() {
    130         synchronized (mLocationLock) {
    131             return mLocation;
    132         }
    133     }
    134 
    135     public FusedPrintersProvider(Activity activity, int internalLoaderId) {
    136         super(activity);
    137         mLocationLock = new Object();
    138         mPersistenceManager = new PersistenceManager(activity, internalLoaderId);
    139         mLocationManager = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE);
    140     }
    141 
    142     public void addHistoricalPrinter(PrinterInfo printer) {
    143         mPersistenceManager.addPrinterAndWritePrinterHistory(printer);
    144     }
    145 
    146     /**
    147      * Add printer to dest, or if updatedPrinters add the updated printer. If the updated printer
    148      * was added, remove it from updatedPrinters.
    149      *
    150      * @param dest The list the printers should be added to
    151      * @param printer The printer to add
    152      * @param updatedPrinters The printer to add
    153      */
    154     private void updateAndAddPrinter(List<PrinterInfo> dest, PrinterInfo printer,
    155             Map<PrinterId, PrinterInfo> updatedPrinters) {
    156         PrinterInfo updatedPrinter = updatedPrinters.remove(printer.getId());
    157         if (updatedPrinter != null) {
    158             dest.add(updatedPrinter);
    159         } else {
    160             dest.add(printer);
    161         }
    162     }
    163 
    164     /**
    165      * Compute the printers, order them appropriately and deliver the printers to the clients. We
    166      * prefer printers that have been previously used (favorites) and printers that have been used
    167      * previously close to the current location (near printers).
    168      *
    169      * @param discoveredPrinters All printers currently discovered by the print discovery session.
    170      * @param favoritePrinters The ordered list of printers. The earlier in the list, the more
    171      *            preferred.
    172      */
    173     private void computeAndDeliverResult(Map<PrinterId, PrinterInfo> discoveredPrinters,
    174             List<Pair<PrinterInfo, Location>> favoritePrinters) {
    175         List<PrinterInfo> printers = new ArrayList<>();
    176 
    177         // Store the printerIds that have already been added. We cannot compare the printerInfos in
    178         // "printers" as they might have been taken from discoveredPrinters and the printerInfo does
    179         // not equals() anymore
    180         HashSet<PrinterId> alreadyAddedPrinter = new HashSet<>(MAX_FAVORITE_PRINTER_COUNT);
    181 
    182         Location location = getCurrentLocation();
    183 
    184         // Add the favorite printers that have last been used close to the current location
    185         final int favoritePrinterCount = favoritePrinters.size();
    186         if (location != null) {
    187             for (int i = 0; i < favoritePrinterCount; i++) {
    188                 // Only add a certain amount of favorite printers
    189                 if (printers.size() == MAX_FAVORITE_PRINTER_COUNT) {
    190                     break;
    191                 }
    192 
    193                 PrinterInfo favoritePrinter = favoritePrinters.get(i).first;
    194                 Location printerLocation = favoritePrinters.get(i).second;
    195 
    196                 if (printerLocation != null
    197                         && !alreadyAddedPrinter.contains(favoritePrinter.getId())) {
    198                     if (printerLocation.distanceTo(location) <= MAX_PRINTER_DISTANCE) {
    199                         updateAndAddPrinter(printers, favoritePrinter, discoveredPrinters);
    200                         alreadyAddedPrinter.add(favoritePrinter.getId());
    201                     }
    202                 }
    203             }
    204         }
    205 
    206         // Add the other favorite printers
    207         for (int i = 0; i < favoritePrinterCount; i++) {
    208             // Only add a certain amount of favorite printers
    209             if (printers.size() == MAX_FAVORITE_PRINTER_COUNT) {
    210                 break;
    211             }
    212 
    213             PrinterInfo favoritePrinter = favoritePrinters.get(i).first;
    214             if (!alreadyAddedPrinter.contains(favoritePrinter.getId())) {
    215                 updateAndAddPrinter(printers, favoritePrinter, discoveredPrinters);
    216                 alreadyAddedPrinter.add(favoritePrinter.getId());
    217             }
    218         }
    219 
    220         // Add other updated printers. Printers that have already been added have been removed from
    221         // discoveredPrinters in the calls to updateAndAddPrinter
    222         final int printerCount = mPrinters.size();
    223         for (int i = 0; i < printerCount; i++) {
    224             PrinterInfo printer = mPrinters.get(i);
    225             PrinterInfo updatedPrinter = discoveredPrinters.remove(
    226                     printer.getId());
    227             if (updatedPrinter != null) {
    228                 printers.add(updatedPrinter);
    229             }
    230         }
    231 
    232         // Add the new printers, i.e. what is left.
    233         printers.addAll(discoveredPrinters.values());
    234 
    235         // Update the list of printers.
    236         mPrinters.clear();
    237         mPrinters.addAll(printers);
    238 
    239         if (isStarted()) {
    240             // If stated deliver the new printers.
    241             deliverResult(printers);
    242         } else {
    243             // Otherwise, take a note for the change.
    244             onContentChanged();
    245         }
    246     }
    247 
    248     @Override
    249     protected void onStartLoading() {
    250         if (DEBUG) {
    251             Log.i(LOG_TAG, "onStartLoading() " + FusedPrintersProvider.this.hashCode());
    252         }
    253 
    254         mLocationManager.requestLocationUpdates(LocationRequest.create()
    255                 .setQuality(LocationRequest.POWER_LOW).setInterval(LOCATION_UPDATE_MS), this,
    256                 Looper.getMainLooper());
    257 
    258         Location lastLocation = mLocationManager.getLastLocation();
    259         if (lastLocation != null) {
    260             onLocationChanged(lastLocation);
    261         }
    262 
    263         // Jumpstart location with a single forced update
    264         Criteria oneTimeCriteria = new Criteria();
    265         oneTimeCriteria.setAccuracy(Criteria.ACCURACY_FINE);
    266         mLocationManager.requestSingleUpdate(oneTimeCriteria, this, Looper.getMainLooper());
    267 
    268         // The contract is that if we already have a valid,
    269         // result the we have to deliver it immediately.
    270         (new Handler(Looper.getMainLooper())).post(new Runnable() {
    271             @Override public void run() {
    272                 deliverResult(new ArrayList<>(mPrinters));
    273             }
    274         });
    275 
    276         // Always load the data to ensure discovery period is
    277         // started and to make sure obsolete printers are updated.
    278         onForceLoad();
    279     }
    280 
    281     @Override
    282     protected void onStopLoading() {
    283         if (DEBUG) {
    284             Log.i(LOG_TAG, "onStopLoading() " + FusedPrintersProvider.this.hashCode());
    285         }
    286         onCancelLoad();
    287 
    288         mLocationManager.removeUpdates(this);
    289     }
    290 
    291     @Override
    292     protected void onForceLoad() {
    293         if (DEBUG) {
    294             Log.i(LOG_TAG, "onForceLoad() " + FusedPrintersProvider.this.hashCode());
    295         }
    296         loadInternal();
    297     }
    298 
    299     private void loadInternal() {
    300         if (mDiscoverySession == null) {
    301             PrintManager printManager = (PrintManager) getContext()
    302                     .getSystemService(Context.PRINT_SERVICE);
    303             mDiscoverySession = printManager.createPrinterDiscoverySession();
    304             mPersistenceManager.readPrinterHistory();
    305         } else if (mPersistenceManager.isHistoryChanged()) {
    306             mPersistenceManager.readPrinterHistory();
    307         }
    308         if (mPersistenceManager.isReadHistoryCompleted()
    309                 && !mDiscoverySession.isPrinterDiscoveryStarted()) {
    310             mDiscoverySession.setOnPrintersChangeListener(new OnPrintersChangeListener() {
    311                 @Override
    312                 public void onPrintersChanged() {
    313                     if (DEBUG) {
    314                         Log.i(LOG_TAG, "onPrintersChanged() count:"
    315                                 + mDiscoverySession.getPrinters().size()
    316                                 + " " + FusedPrintersProvider.this.hashCode());
    317                     }
    318 
    319                     updatePrinters(mDiscoverySession.getPrinters(), mFavoritePrinters,
    320                             getCurrentLocation());
    321                 }
    322             });
    323             final int favoriteCount = mFavoritePrinters.size();
    324             List<PrinterId> printerIds = new ArrayList<>(favoriteCount);
    325             for (int i = 0; i < favoriteCount; i++) {
    326                 printerIds.add(mFavoritePrinters.get(i).first.getId());
    327             }
    328             mDiscoverySession.startPrinterDiscovery(printerIds);
    329             List<PrinterInfo> printers = mDiscoverySession.getPrinters();
    330 
    331             updatePrinters(printers, mFavoritePrinters, getCurrentLocation());
    332         }
    333     }
    334 
    335     private void updatePrinters(List<PrinterInfo> printers,
    336             List<Pair<PrinterInfo, Location>> favoritePrinters,
    337             Location location) {
    338         if (mPrintersUpdatedBefore && mPrinters.equals(printers)
    339                 && mFavoritePrinters.equals(favoritePrinters)
    340                 && Objects.equals(mLocationOfLastPrinterUpdate, location)) {
    341             return;
    342         }
    343 
    344         mLocationOfLastPrinterUpdate = location;
    345         mPrintersUpdatedBefore = true;
    346 
    347         // Some of the found printers may have be a printer that is in the
    348         // history but with its properties changed. Hence, we try to update the
    349         // printer to use its current properties instead of the historical one.
    350         mPersistenceManager.updateHistoricalPrintersIfNeeded(printers);
    351 
    352         Map<PrinterId, PrinterInfo> printersMap = new LinkedHashMap<>();
    353         final int printerCount = printers.size();
    354         for (int i = 0; i < printerCount; i++) {
    355             PrinterInfo printer = printers.get(i);
    356             printersMap.put(printer.getId(), printer);
    357         }
    358 
    359         computeAndDeliverResult(printersMap, favoritePrinters);
    360     }
    361 
    362     @Override
    363     protected boolean onCancelLoad() {
    364         if (DEBUG) {
    365             Log.i(LOG_TAG, "onCancelLoad() " + FusedPrintersProvider.this.hashCode());
    366         }
    367         return cancelInternal();
    368     }
    369 
    370     private boolean cancelInternal() {
    371         if (mDiscoverySession != null
    372                 && mDiscoverySession.isPrinterDiscoveryStarted()) {
    373             if (mTrackedPrinter != null) {
    374                 mDiscoverySession.stopPrinterStateTracking(mTrackedPrinter);
    375                 mTrackedPrinter = null;
    376             }
    377             mDiscoverySession.stopPrinterDiscovery();
    378             return true;
    379         } else if (mPersistenceManager.isReadHistoryInProgress()) {
    380             return mPersistenceManager.stopReadPrinterHistory();
    381         }
    382         return false;
    383     }
    384 
    385     @Override
    386     protected void onReset() {
    387         if (DEBUG) {
    388             Log.i(LOG_TAG, "onReset() " + FusedPrintersProvider.this.hashCode());
    389         }
    390         onStopLoading();
    391         mPrinters.clear();
    392         if (mDiscoverySession != null) {
    393             mDiscoverySession.destroy();
    394         }
    395     }
    396 
    397     @Override
    398     protected void onAbandon() {
    399         if (DEBUG) {
    400             Log.i(LOG_TAG, "onAbandon() " + FusedPrintersProvider.this.hashCode());
    401         }
    402         onStopLoading();
    403     }
    404 
    405     /**
    406      * Check if the location is acceptable. This is to filter out excessively old or inaccurate
    407      * location updates.
    408      *
    409      * @param location the location to check
    410      * @return true iff the location is usable.
    411      */
    412     private boolean isLocationAcceptable(Location location) {
    413         return location != null
    414                 && location.getElapsedRealtimeNanos() > SystemClock.elapsedRealtimeNanos()
    415                         - MAX_LOCATION_AGE_MS * 1000_000L
    416                 && location.hasAccuracy()
    417                 && location.getAccuracy() < MIN_LOCATION_ACCURACY;
    418     }
    419 
    420     @Override
    421     public void onLocationChanged(Location location) {
    422         synchronized(mLocationLock) {
    423             // We expect the user to not move too fast while printing. Hence prefer more accurate
    424             // updates over more recent ones for LOCATION_UPDATE_MS. We add a 10% fudge factor here
    425             // as the location provider might send an update slightly too early.
    426             if (isLocationAcceptable(location)
    427                     && !location.equals(mLocation)
    428                     && (mLocation == null
    429                             || location
    430                                     .getElapsedRealtimeNanos() > mLocation.getElapsedRealtimeNanos()
    431                                             + LOCATION_UPDATE_MS * 0.9 * 1000_000L
    432                             || (!mLocation.hasAccuracy()
    433                                     || location.getAccuracy() < mLocation.getAccuracy()))) {
    434                 // Other callers of updatePrinters might want to know the location, hence cache it
    435                 mLocation = location;
    436 
    437                 if (areHistoricalPrintersLoaded()) {
    438                     updatePrinters(mDiscoverySession.getPrinters(), mFavoritePrinters, mLocation);
    439                 }
    440             }
    441         }
    442     }
    443 
    444     @Override
    445     public void onStatusChanged(String provider, int status, Bundle extras) {
    446         // nothing to do
    447     }
    448 
    449     @Override
    450     public void onProviderEnabled(String provider) {
    451         // nothing to do
    452     }
    453 
    454     @Override
    455     public void onProviderDisabled(String provider) {
    456         // nothing to do
    457     }
    458 
    459     public boolean areHistoricalPrintersLoaded() {
    460         return mPersistenceManager.mReadHistoryCompleted;
    461     }
    462 
    463     public void setTrackedPrinter(@Nullable PrinterId printerId) {
    464         if (isStarted() && mDiscoverySession != null
    465                 && mDiscoverySession.isPrinterDiscoveryStarted()) {
    466             if (mTrackedPrinter != null) {
    467                 if (mTrackedPrinter.equals(printerId)) {
    468                     return;
    469                 }
    470                 mDiscoverySession.stopPrinterStateTracking(mTrackedPrinter);
    471             }
    472             mTrackedPrinter = printerId;
    473             if (printerId != null) {
    474                 mDiscoverySession.startPrinterStateTracking(printerId);
    475             }
    476         }
    477     }
    478 
    479     public boolean isFavoritePrinter(PrinterId printerId) {
    480         final int printerCount = mFavoritePrinters.size();
    481         for (int i = 0; i < printerCount; i++) {
    482             PrinterInfo favoritePritner = mFavoritePrinters.get(i).first;
    483             if (favoritePritner.getId().equals(printerId)) {
    484                 return true;
    485             }
    486         }
    487         return false;
    488     }
    489 
    490     public void forgetFavoritePrinter(PrinterId printerId) {
    491         final int favoritePrinterCount = mFavoritePrinters.size();
    492         List<Pair<PrinterInfo, Location>> newFavoritePrinters = new ArrayList<>(
    493                 favoritePrinterCount - 1);
    494 
    495         // Remove the printer from the favorites.
    496         for (int i = 0; i < favoritePrinterCount; i++) {
    497             if (!mFavoritePrinters.get(i).first.getId().equals(printerId)) {
    498                 newFavoritePrinters.add(mFavoritePrinters.get(i));
    499             }
    500         }
    501 
    502         // Remove the printer from history and persist the latter.
    503         mPersistenceManager.removeHistoricalPrinterAndWritePrinterHistory(printerId);
    504 
    505         // Recompute and deliver the printers.
    506         updatePrinters(mDiscoverySession.getPrinters(), newFavoritePrinters, getCurrentLocation());
    507     }
    508 
    509     private final class PersistenceManager implements
    510             LoaderManager.LoaderCallbacks<List<PrintServiceInfo>> {
    511         private static final String PERSIST_FILE_NAME = "printer_history.xml";
    512 
    513         private static final String TAG_PRINTERS = "printers";
    514 
    515         private static final String TAG_PRINTER = "printer";
    516         private static final String TAG_LOCATION = "location";
    517         private static final String TAG_PRINTER_ID = "printerId";
    518 
    519         private static final String ATTR_LOCAL_ID = "localId";
    520         private static final String ATTR_SERVICE_NAME = "serviceName";
    521 
    522         private static final String ATTR_LONGITUDE = "longitude";
    523         private static final String ATTR_LATITUDE = "latitude";
    524         private static final String ATTR_ACCURACY = "accuracy";
    525 
    526         private static final String ATTR_NAME = "name";
    527         private static final String ATTR_DESCRIPTION = "description";
    528 
    529         private final AtomicFile mStatePersistFile;
    530 
    531         /**
    532          * Whether the enabled print services have been updated since last time the history was
    533          * read.
    534          */
    535         private boolean mAreEnabledServicesUpdated;
    536 
    537         /** The enabled services read when they were last updated */
    538         private @NonNull List<PrintServiceInfo> mEnabledServices;
    539 
    540         private List<Pair<PrinterInfo, Location>> mHistoricalPrinters = new ArrayList<>();
    541 
    542         private boolean mReadHistoryCompleted;
    543 
    544         private ReadTask mReadTask;
    545 
    546         private volatile long mLastReadHistoryTimestamp;
    547 
    548         private PersistenceManager(final Activity activity, final int internalLoaderId) {
    549             mStatePersistFile = new AtomicFile(new File(activity.getFilesDir(),
    550                     PERSIST_FILE_NAME));
    551 
    552             // Initialize enabled services to make sure they are set are the read task might be done
    553             // before the loader updated the services the first time.
    554             mEnabledServices = ((PrintManager) activity
    555                     .getSystemService(Context.PRINT_SERVICE))
    556                     .getPrintServices(PrintManager.ENABLED_SERVICES);
    557 
    558             mAreEnabledServicesUpdated = true;
    559 
    560             // Cannot start a loader while starting another, hence delay this loader
    561             (new Handler(activity.getMainLooper())).post(new Runnable() {
    562                 @Override
    563                 public void run() {
    564                     activity.getLoaderManager().initLoader(internalLoaderId, null,
    565                             PersistenceManager.this);
    566                 }
    567             });
    568         }
    569 
    570 
    571         @Override
    572         public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
    573             return new PrintServicesLoader(
    574                     (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE),
    575                     getContext(), PrintManager.ENABLED_SERVICES);
    576         }
    577 
    578         @Override
    579         public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
    580                 List<PrintServiceInfo> services) {
    581             mAreEnabledServicesUpdated = true;
    582             mEnabledServices = services;
    583 
    584             // Ask the fused printer provider to reload which will cause the persistence manager to
    585             // reload the history and reconsider the enabled services.
    586             if (isStarted()) {
    587                 forceLoad();
    588             }
    589         }
    590 
    591         @Override
    592         public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
    593             // no data is cached
    594         }
    595 
    596         public boolean isReadHistoryInProgress() {
    597             return mReadTask != null;
    598         }
    599 
    600         public boolean isReadHistoryCompleted() {
    601             return mReadHistoryCompleted;
    602         }
    603 
    604         public boolean stopReadPrinterHistory() {
    605             return mReadTask.cancel(true);
    606         }
    607 
    608         public void readPrinterHistory() {
    609             if (DEBUG) {
    610                 Log.i(LOG_TAG, "read history started "
    611                         + FusedPrintersProvider.this.hashCode());
    612             }
    613             mReadTask = new ReadTask();
    614             mReadTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
    615         }
    616 
    617         public void updateHistoricalPrintersIfNeeded(List<PrinterInfo> printers) {
    618             boolean writeHistory = false;
    619 
    620             final int printerCount = printers.size();
    621             for (int i = 0; i < printerCount; i++) {
    622                 PrinterInfo printer = printers.get(i);
    623                 writeHistory |= updateHistoricalPrinterIfNeeded(printer);
    624             }
    625 
    626             if (writeHistory) {
    627                 writePrinterHistory();
    628             }
    629         }
    630 
    631         /**
    632          * Updates the historical printer state with the given printer.
    633          *
    634          * @param printer the printer to update
    635          *
    636          * @return true iff the historical printer list needs to be updated
    637          */
    638         public boolean updateHistoricalPrinterIfNeeded(PrinterInfo printer) {
    639             boolean writeHistory = false;
    640             final int printerCount = mHistoricalPrinters.size();
    641             for (int i = 0; i < printerCount; i++) {
    642                 PrinterInfo historicalPrinter = mHistoricalPrinters.get(i).first;
    643 
    644                 if (!historicalPrinter.getId().equals(printer.getId())) {
    645                     continue;
    646                 }
    647 
    648                 // Overwrite the historical printer with the updated printer as some properties
    649                 // changed. We ignore the status as this is a volatile state.
    650                 if (historicalPrinter.equalsIgnoringStatus(printer)) {
    651                     continue;
    652                 }
    653 
    654                 mHistoricalPrinters.set(i, new Pair<PrinterInfo, Location>(printer,
    655                                 mHistoricalPrinters.get(i).second));
    656 
    657                 // We only persist limited information in the printer history, hence check if
    658                 // we need to persist the update.
    659                 // @see PersistenceManager.WriteTask#doWritePrinterHistory
    660                 if (!historicalPrinter.getName().equals(printer.getName())) {
    661                     if (Objects.equals(historicalPrinter.getDescription(),
    662                             printer.getDescription())) {
    663                         writeHistory = true;
    664                     }
    665                 }
    666             }
    667             return writeHistory;
    668         }
    669 
    670         public void addPrinterAndWritePrinterHistory(PrinterInfo printer) {
    671             if (mHistoricalPrinters.size() >= MAX_HISTORY_LENGTH) {
    672                 mHistoricalPrinters.remove(0);
    673             }
    674 
    675             Location location = getCurrentLocation();
    676             if (!isLocationAcceptable(location)) {
    677                 location = null;
    678             }
    679 
    680             mHistoricalPrinters.add(new Pair<PrinterInfo, Location>(printer, location));
    681 
    682             writePrinterHistory();
    683         }
    684 
    685         public void removeHistoricalPrinterAndWritePrinterHistory(PrinterId printerId) {
    686             boolean writeHistory = false;
    687             final int printerCount = mHistoricalPrinters.size();
    688             for (int i = printerCount - 1; i >= 0; i--) {
    689                 PrinterInfo historicalPrinter = mHistoricalPrinters.get(i).first;
    690                 if (historicalPrinter.getId().equals(printerId)) {
    691                     mHistoricalPrinters.remove(i);
    692                     writeHistory = true;
    693                 }
    694             }
    695             if (writeHistory) {
    696                 writePrinterHistory();
    697             }
    698         }
    699 
    700         @SuppressWarnings("unchecked")
    701         private void writePrinterHistory() {
    702             new WriteTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,
    703                     new ArrayList<>(mHistoricalPrinters));
    704         }
    705 
    706         public boolean isHistoryChanged() {
    707             return mAreEnabledServicesUpdated ||
    708                     mLastReadHistoryTimestamp != mStatePersistFile.getBaseFile().lastModified();
    709         }
    710 
    711         /**
    712          * Sort the favorite printers by weight. If a printer is in the list multiple times for
    713          * different locations, all instances are considered to have the accumulative weight. The
    714          * actual favorite printers to display are computed in {@link #computeAndDeliverResult} as
    715          * only at this time we know the location to use to determine if a printer is close enough
    716          * to be preferred.
    717          *
    718          * @param printers The printers to sort.
    719          * @return The sorted printers.
    720          */
    721         private List<Pair<PrinterInfo, Location>> sortFavoritePrinters(
    722                 List<Pair<PrinterInfo, Location>> printers) {
    723             Map<PrinterId, PrinterRecord> recordMap = new ArrayMap<>();
    724 
    725             // Compute the weights.
    726             float currentWeight = 1.0f;
    727             final int printerCount = printers.size();
    728             for (int i = printerCount - 1; i >= 0; i--) {
    729                 PrinterId printerId = printers.get(i).first.getId();
    730                 PrinterRecord record = recordMap.get(printerId);
    731                 if (record == null) {
    732                     record = new PrinterRecord();
    733                     recordMap.put(printerId, record);
    734                 }
    735 
    736                 record.printers.add(printers.get(i));
    737 
    738                 // Aggregate weight for the same printer
    739                 record.weight += currentWeight;
    740                 currentWeight *= WEIGHT_DECAY_COEFFICIENT;
    741             }
    742 
    743             // Sort the favorite printers.
    744             List<PrinterRecord> favoriteRecords = new ArrayList<>(
    745                     recordMap.values());
    746             Collections.sort(favoriteRecords);
    747 
    748             // Write the favorites to the output.
    749             final int recordCount = favoriteRecords.size();
    750             List<Pair<PrinterInfo, Location>> favoritePrinters = new ArrayList<>(printerCount);
    751             for (int i = 0; i < recordCount; i++) {
    752                 favoritePrinters.addAll(favoriteRecords.get(i).printers);
    753             }
    754 
    755             return favoritePrinters;
    756         }
    757 
    758         /**
    759          * A set of printers with the same ID and the weight associated with them during
    760          * {@link #sortFavoritePrinters}.
    761          */
    762         private final class PrinterRecord implements Comparable<PrinterRecord> {
    763             /**
    764              * The printers, all with the same ID, but potentially different properties or locations
    765              */
    766             public final List<Pair<PrinterInfo, Location>> printers;
    767 
    768             /** The weight associated with the printers */
    769             public float weight;
    770 
    771             /**
    772              * Create a new record.
    773              */
    774             public PrinterRecord() {
    775                 printers = new ArrayList<>();
    776             }
    777 
    778             /**
    779              * Compare two records by weight.
    780              */
    781             @Override
    782             public int compareTo(PrinterRecord another) {
    783                 return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
    784             }
    785         }
    786 
    787         private final class ReadTask
    788                 extends AsyncTask<Void, Void, List<Pair<PrinterInfo, Location>>> {
    789             @Override
    790             protected List<Pair<PrinterInfo, Location>> doInBackground(Void... args) {
    791                return doReadPrinterHistory();
    792             }
    793 
    794             @Override
    795             protected void onPostExecute(List<Pair<PrinterInfo, Location>> printers) {
    796                 if (DEBUG) {
    797                     Log.i(LOG_TAG, "read history completed "
    798                             + FusedPrintersProvider.this.hashCode());
    799                 }
    800 
    801                 // Ignore printer records whose target services are not enabled.
    802                 Set<ComponentName> enabledComponents = new ArraySet<>();
    803                 final int installedServiceCount = mEnabledServices.size();
    804                 for (int i = 0; i < installedServiceCount; i++) {
    805                     ServiceInfo serviceInfo = mEnabledServices.get(i).getResolveInfo().serviceInfo;
    806                     ComponentName componentName = new ComponentName(
    807                             serviceInfo.packageName, serviceInfo.name);
    808                     enabledComponents.add(componentName);
    809                 }
    810                 mAreEnabledServicesUpdated = false;
    811 
    812                 final int printerCount = printers.size();
    813                 for (int i = printerCount - 1; i >= 0; i--) {
    814                     ComponentName printerServiceName = printers.get(i).first.getId()
    815                             .getServiceName();
    816                     if (!enabledComponents.contains(printerServiceName)) {
    817                         printers.remove(i);
    818                     }
    819                 }
    820 
    821                 // Store the filtered list.
    822                 mHistoricalPrinters = printers;
    823 
    824                 // Compute the favorite printers.
    825                 mFavoritePrinters.clear();
    826                 mFavoritePrinters.addAll(sortFavoritePrinters(mHistoricalPrinters));
    827 
    828                 mReadHistoryCompleted = true;
    829 
    830                 // Deliver the printers.
    831                 updatePrinters(mDiscoverySession.getPrinters(), mFavoritePrinters,
    832                         getCurrentLocation());
    833 
    834                 // We are done.
    835                 mReadTask = null;
    836 
    837                 // Loading the available printers if needed.
    838                 loadInternal();
    839             }
    840 
    841             @Override
    842             protected void onCancelled(List<Pair<PrinterInfo, Location>> printerInfos) {
    843                 // We are done.
    844                 mReadTask = null;
    845             }
    846 
    847             private List<Pair<PrinterInfo, Location>> doReadPrinterHistory() {
    848                 final FileInputStream in;
    849                 try {
    850                     in = mStatePersistFile.openRead();
    851                 } catch (FileNotFoundException fnfe) {
    852                     if (DEBUG) {
    853                         Log.i(LOG_TAG, "No existing printer history "
    854                                 + FusedPrintersProvider.this.hashCode());
    855                     }
    856                     return new ArrayList<>();
    857                 }
    858                 try {
    859                     List<Pair<PrinterInfo, Location>> printers = new ArrayList<>();
    860                     XmlPullParser parser = Xml.newPullParser();
    861                     parser.setInput(in, StandardCharsets.UTF_8.name());
    862                     parseState(parser, printers);
    863                     // Take a note which version of the history was read.
    864                     mLastReadHistoryTimestamp = mStatePersistFile.getBaseFile().lastModified();
    865                     return printers;
    866                 } catch (IllegalStateException
    867                         | NullPointerException
    868                         | NumberFormatException
    869                         | XmlPullParserException
    870                         | IOException
    871                         | IndexOutOfBoundsException e) {
    872                     Slog.w(LOG_TAG, "Failed parsing ", e);
    873                 } finally {
    874                     IoUtils.closeQuietly(in);
    875                 }
    876 
    877                 return Collections.emptyList();
    878             }
    879 
    880             private void parseState(XmlPullParser parser,
    881                     List<Pair<PrinterInfo, Location>> outPrinters)
    882                             throws IOException, XmlPullParserException {
    883                 parser.next();
    884                 skipEmptyTextTags(parser);
    885                 expect(parser, XmlPullParser.START_TAG, TAG_PRINTERS);
    886                 parser.next();
    887 
    888                 while (parsePrinter(parser, outPrinters)) {
    889                     // Be nice and respond to cancellation
    890                     if (isCancelled()) {
    891                         return;
    892                     }
    893                     parser.next();
    894                 }
    895 
    896                 skipEmptyTextTags(parser);
    897                 expect(parser, XmlPullParser.END_TAG, TAG_PRINTERS);
    898             }
    899 
    900             private boolean parsePrinter(XmlPullParser parser,
    901                     List<Pair<PrinterInfo, Location>> outPrinters)
    902                             throws IOException, XmlPullParserException {
    903                 skipEmptyTextTags(parser);
    904                 if (!accept(parser, XmlPullParser.START_TAG, TAG_PRINTER)) {
    905                     return false;
    906                 }
    907 
    908                 String name = parser.getAttributeValue(null, ATTR_NAME);
    909                 String description = parser.getAttributeValue(null, ATTR_DESCRIPTION);
    910 
    911                 parser.next();
    912 
    913                 skipEmptyTextTags(parser);
    914                 expect(parser, XmlPullParser.START_TAG, TAG_PRINTER_ID);
    915                 String localId = parser.getAttributeValue(null, ATTR_LOCAL_ID);
    916                 ComponentName service = ComponentName.unflattenFromString(parser.getAttributeValue(
    917                         null, ATTR_SERVICE_NAME));
    918                 PrinterId printerId =  new PrinterId(service, localId);
    919                 parser.next();
    920                 skipEmptyTextTags(parser);
    921                 expect(parser, XmlPullParser.END_TAG, TAG_PRINTER_ID);
    922                 parser.next();
    923 
    924                 skipEmptyTextTags(parser);
    925                 Location location;
    926                 if (accept(parser, XmlPullParser.START_TAG, TAG_LOCATION)) {
    927                     location = new Location("");
    928                     location.setLongitude(
    929                             Double.parseDouble(parser.getAttributeValue(null, ATTR_LONGITUDE)));
    930                     location.setLatitude(
    931                             Double.parseDouble(parser.getAttributeValue(null, ATTR_LATITUDE)));
    932                     location.setAccuracy(
    933                             Float.parseFloat(parser.getAttributeValue(null, ATTR_ACCURACY)));
    934                     parser.next();
    935 
    936                     skipEmptyTextTags(parser);
    937                     expect(parser, XmlPullParser.END_TAG, TAG_LOCATION);
    938                     parser.next();
    939                 } else {
    940                     location = null;
    941                 }
    942 
    943                 // If the printer is available the printer will be replaced by the one read from the
    944                 // discovery session, hence the only time when this object is used is when the
    945                 // printer is unavailable.
    946                 PrinterInfo.Builder builder = new PrinterInfo.Builder(printerId, name,
    947                         PrinterInfo.STATUS_UNAVAILABLE);
    948                 builder.setDescription(description);
    949                 PrinterInfo printer = builder.build();
    950 
    951                 outPrinters.add(new Pair<PrinterInfo, Location>(printer, location));
    952 
    953                 if (DEBUG) {
    954                     Log.i(LOG_TAG, "[RESTORED] " + printer);
    955                 }
    956 
    957                 skipEmptyTextTags(parser);
    958                 expect(parser, XmlPullParser.END_TAG, TAG_PRINTER);
    959 
    960                 return true;
    961             }
    962 
    963             private void expect(XmlPullParser parser, int type, String tag)
    964                     throws XmlPullParserException {
    965                 if (!accept(parser, type, tag)) {
    966                     throw new XmlPullParserException("Exepected event: " + type
    967                             + " and tag: " + tag + " but got event: " + parser.getEventType()
    968                             + " and tag:" + parser.getName());
    969                 }
    970             }
    971 
    972             private void skipEmptyTextTags(XmlPullParser parser)
    973                     throws IOException, XmlPullParserException {
    974                 while (accept(parser, XmlPullParser.TEXT, null)
    975                         && "\n".equals(parser.getText())) {
    976                     parser.next();
    977                 }
    978             }
    979 
    980             private boolean accept(XmlPullParser parser, int type, String tag)
    981                     throws XmlPullParserException {
    982                 if (parser.getEventType() != type) {
    983                     return false;
    984                 }
    985                 if (tag != null) {
    986                     if (!tag.equals(parser.getName())) {
    987                         return false;
    988                     }
    989                 } else if (parser.getName() != null) {
    990                     return false;
    991                 }
    992                 return true;
    993             }
    994         }
    995 
    996         private final class WriteTask
    997                 extends AsyncTask<List<Pair<PrinterInfo, Location>>, Void, Void> {
    998             @Override
    999             protected Void doInBackground(
   1000                     @SuppressWarnings("unchecked") List<Pair<PrinterInfo, Location>>... printers) {
   1001                 doWritePrinterHistory(printers[0]);
   1002                 return null;
   1003             }
   1004 
   1005             private void doWritePrinterHistory(List<Pair<PrinterInfo, Location>> printers) {
   1006                 FileOutputStream out = null;
   1007                 try {
   1008                     out = mStatePersistFile.startWrite();
   1009 
   1010                     XmlSerializer serializer = new FastXmlSerializer();
   1011                     serializer.setOutput(out, StandardCharsets.UTF_8.name());
   1012                     serializer.startDocument(null, true);
   1013                     serializer.startTag(null, TAG_PRINTERS);
   1014 
   1015                     final int printerCount = printers.size();
   1016                     for (int i = 0; i < printerCount; i++) {
   1017                         PrinterInfo printer = printers.get(i).first;
   1018 
   1019                         serializer.startTag(null, TAG_PRINTER);
   1020 
   1021                         serializer.attribute(null, ATTR_NAME, printer.getName());
   1022                         String description = printer.getDescription();
   1023                         if (description != null) {
   1024                             serializer.attribute(null, ATTR_DESCRIPTION, description);
   1025                         }
   1026 
   1027                         PrinterId printerId = printer.getId();
   1028                         serializer.startTag(null, TAG_PRINTER_ID);
   1029                         serializer.attribute(null, ATTR_LOCAL_ID, printerId.getLocalId());
   1030                         serializer.attribute(null, ATTR_SERVICE_NAME, printerId.getServiceName()
   1031                                 .flattenToString());
   1032                         serializer.endTag(null, TAG_PRINTER_ID);
   1033 
   1034                         Location location = printers.get(i).second;
   1035                         if (location != null) {
   1036                             serializer.startTag(null, TAG_LOCATION);
   1037                             serializer.attribute(null, ATTR_LONGITUDE,
   1038                                     String.valueOf(location.getLongitude()));
   1039                             serializer.attribute(null, ATTR_LATITUDE,
   1040                                     String.valueOf(location.getLatitude()));
   1041                             serializer.attribute(null, ATTR_ACCURACY,
   1042                                     String.valueOf(location.getAccuracy()));
   1043                             serializer.endTag(null, TAG_LOCATION);
   1044                         }
   1045 
   1046                         serializer.endTag(null, TAG_PRINTER);
   1047 
   1048                         if (DEBUG) {
   1049                             Log.i(LOG_TAG, "[PERSISTED] " + printer);
   1050                         }
   1051                     }
   1052 
   1053                     serializer.endTag(null, TAG_PRINTERS);
   1054                     serializer.endDocument();
   1055                     mStatePersistFile.finishWrite(out);
   1056 
   1057                     if (DEBUG) {
   1058                         Log.i(LOG_TAG, "[PERSIST END]");
   1059                     }
   1060                 } catch (IOException ioe) {
   1061                     Slog.w(LOG_TAG, "Failed to write printer history, restoring backup.", ioe);
   1062                     mStatePersistFile.failWrite(out);
   1063                 } finally {
   1064                     IoUtils.closeQuietly(out);
   1065                 }
   1066             }
   1067         }
   1068     }
   1069 }
   1070