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