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