Home | History | Annotate | Download | only in bips
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  * Copyright (C) 2016 Mopria Alliance, Inc.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.bips;
     19 
     20 import android.print.PrintManager;
     21 import android.print.PrinterId;
     22 import android.print.PrinterInfo;
     23 import android.printservice.PrintServiceInfo;
     24 import android.printservice.PrinterDiscoverySession;
     25 import android.printservice.recommendation.RecommendationInfo;
     26 import android.util.ArrayMap;
     27 import android.util.ArraySet;
     28 import android.util.JsonReader;
     29 import android.util.JsonWriter;
     30 import android.util.Log;
     31 
     32 import com.android.bips.discovery.DiscoveredPrinter;
     33 import com.android.bips.discovery.Discovery;
     34 import com.android.bips.ipp.CapabilitiesCache;
     35 
     36 import java.io.File;
     37 import java.io.FileReader;
     38 import java.io.FileWriter;
     39 import java.io.IOException;
     40 import java.net.InetAddress;
     41 import java.net.UnknownHostException;
     42 import java.util.ArrayList;
     43 import java.util.Collections;
     44 import java.util.HashMap;
     45 import java.util.HashSet;
     46 import java.util.List;
     47 import java.util.Map;
     48 import java.util.Set;
     49 
     50 class LocalDiscoverySession extends PrinterDiscoverySession implements Discovery.Listener,
     51         PrintManager.PrintServiceRecommendationsChangeListener,
     52         PrintManager.PrintServicesChangeListener {
     53     private static final String TAG = LocalDiscoverySession.class.getSimpleName();
     54     private static final boolean DEBUG = false;
     55 
     56     // Printers are removed after not being seen for this long
     57     static final long PRINTER_EXPIRATION_MILLIS = 3000;
     58 
     59     private static final String KNOWN_GOOD_FILE = "knowngood.json";
     60     private static final int KNOWN_GOOD_MAX = 50;
     61 
     62     private final BuiltInPrintService mPrintService;
     63     private final CapabilitiesCache mCapabilitiesCache;
     64     private final Map<PrinterId, LocalPrinter> mPrinters = new HashMap<>();
     65     private final Set<PrinterId> mPriorityIds = new HashSet<>();
     66     private final Set<PrinterId> mTrackingIds = new HashSet<>();
     67     private final List<PrinterId> mKnownGood = new ArrayList<>();
     68     private Runnable mExpirePrinters;
     69 
     70     PrintManager mPrintManager;
     71 
     72     /** Package names of all currently enabled print services beside this one */
     73     private ArraySet<String> mEnabledServices = new ArraySet<>();
     74 
     75     /**
     76      * Address of printers that can be handled by print services, ordered by package name of the
     77      * print service. The print service might not be enabled. For that, look at
     78      * {@link #mEnabledServices}.
     79      *
     80      * <p>This print service only shows a printer if another print service does not show it.
     81      */
     82     private final ArrayMap<InetAddress, ArrayList<String>> mPrintersOfOtherService =
     83             new ArrayMap<>();
     84 
     85     LocalDiscoverySession(BuiltInPrintService service) {
     86         mPrintService = service;
     87         mCapabilitiesCache = service.getCapabilitiesCache();
     88         mPrintManager = mPrintService.getSystemService(PrintManager.class);
     89         loadKnownGood();
     90     }
     91 
     92     @Override
     93     public void onStartPrinterDiscovery(List<PrinterId> priorityList) {
     94         if (DEBUG) Log.d(TAG, "onStartPrinterDiscovery() " + priorityList);
     95 
     96         // Replace priority IDs with the current list.
     97         mPriorityIds.clear();
     98         mPriorityIds.addAll(priorityList);
     99 
    100         // Mark all known printers as "not found". They may return shortly or may expire
    101         mPrinters.values().forEach(LocalPrinter::notFound);
    102         monitorExpiredPrinters();
    103 
    104         mPrintService.getDiscovery().start(this);
    105 
    106         mPrintManager.addPrintServicesChangeListener(this, null);
    107         onPrintServicesChanged();
    108 
    109         mPrintManager.addPrintServiceRecommendationsChangeListener(this, null);
    110         onPrintServiceRecommendationsChanged();
    111     }
    112 
    113     @Override
    114     public void onStopPrinterDiscovery() {
    115         if (DEBUG) Log.d(TAG, "onStopPrinterDiscovery()");
    116         mPrintService.getDiscovery().stop(this);
    117 
    118         PrintManager printManager = mPrintService.getSystemService(PrintManager.class);
    119         printManager.removePrintServicesChangeListener(this);
    120         printManager.removePrintServiceRecommendationsChangeListener(this);
    121 
    122         if (mExpirePrinters != null) {
    123             mPrintService.getMainHandler().removeCallbacks(mExpirePrinters);
    124             mExpirePrinters = null;
    125         }
    126     }
    127 
    128     @Override
    129     public void onValidatePrinters(List<PrinterId> printerIds) {
    130         if (DEBUG) Log.d(TAG, "onValidatePrinters() " + printerIds);
    131     }
    132 
    133     @Override
    134     public void onStartPrinterStateTracking(final PrinterId printerId) {
    135         if (DEBUG) Log.d(TAG, "onStartPrinterStateTracking() " + printerId);
    136         LocalPrinter localPrinter = mPrinters.get(printerId);
    137         mTrackingIds.add(printerId);
    138 
    139         // We cannot track the printer yet; wait until it is discovered
    140         if (localPrinter == null || !localPrinter.isFound()) return;
    141 
    142         // Immediately request a refresh of capabilities
    143         localPrinter.requestCapabilities();
    144     }
    145 
    146     @Override
    147     public void onStopPrinterStateTracking(PrinterId printerId) {
    148         if (DEBUG) Log.d(TAG, "onStopPrinterStateTracking() " + printerId.getLocalId());
    149         mTrackingIds.remove(printerId);
    150     }
    151 
    152     @Override
    153     public void onDestroy() {
    154         if (DEBUG) Log.d(TAG, "onDestroy");
    155         saveKnownGood();
    156     }
    157 
    158     /**
    159      * A printer was found during discovery
    160      */
    161     @Override
    162     public void onPrinterFound(DiscoveredPrinter discoveredPrinter) {
    163         if (DEBUG) Log.d(TAG, "onPrinterFound() " + discoveredPrinter);
    164         if (isDestroyed()) {
    165             Log.w(TAG, "Destroyed; ignoring");
    166             return;
    167         }
    168 
    169         final PrinterId printerId = discoveredPrinter.getId(mPrintService);
    170         LocalPrinter localPrinter = mPrinters.get(printerId);
    171         if (localPrinter == null) {
    172             localPrinter = new LocalPrinter(mPrintService, this, discoveredPrinter);
    173             mPrinters.put(printerId, localPrinter);
    174         }
    175         localPrinter.found();
    176     }
    177 
    178     /**
    179      * A printer was lost during discovery
    180      */
    181     @Override
    182     public void onPrinterLost(DiscoveredPrinter lostPrinter) {
    183         if (DEBUG) Log.d(TAG, "onPrinterLost() " + lostPrinter);
    184 
    185         PrinterId printerId = lostPrinter.getId(mPrintService);
    186         if (printerId.getLocalId().startsWith("ipp")) {
    187             // Forget capabilities for network addresses (which are not globally unique)
    188             mCapabilitiesCache.remove(lostPrinter.getUri());
    189         }
    190 
    191         LocalPrinter localPrinter = mPrinters.get(printerId);
    192         if (localPrinter == null) return;
    193 
    194         localPrinter.notFound();
    195         handlePrinter(localPrinter);
    196         monitorExpiredPrinters();
    197     }
    198 
    199     private void monitorExpiredPrinters() {
    200         if (mExpirePrinters == null && !mPrinters.isEmpty()) {
    201             mExpirePrinters = new ExpirePrinters();
    202             mPrintService.getMainHandler().postDelayed(mExpirePrinters, PRINTER_EXPIRATION_MILLIS);
    203         }
    204     }
    205 
    206     /** A complete printer record is available */
    207     void handlePrinter(LocalPrinter localPrinter) {
    208         if (localPrinter.getCapabilities() == null &&
    209                 !mKnownGood.contains(localPrinter.getPrinterId())) {
    210             // Ignore printers that have no capabilities and are not known-good
    211             return;
    212         }
    213 
    214         PrinterInfo info = localPrinter.createPrinterInfo();
    215 
    216         mKnownGood.remove(localPrinter.getPrinterId());
    217 
    218         if (info == null) return;
    219 
    220         // Update known-good database with current results.
    221         if (info.getStatus() == PrinterInfo.STATUS_IDLE && localPrinter.getUuid() != null) {
    222             // Mark UUID-based printers with IDLE status as known-good
    223             mKnownGood.add(0, localPrinter.getPrinterId());
    224         }
    225 
    226         for (PrinterInfo knownInfo : getPrinters()) {
    227             if (knownInfo.getId().equals(info.getId()) && (info.getCapabilities() == null)) {
    228                 if (DEBUG) Log.d(TAG, "Ignore update with no caps " + localPrinter);
    229                 return;
    230             }
    231         }
    232 
    233         if (DEBUG) {
    234             Log.d(TAG, "handlePrinter: reporting " + localPrinter +
    235                     " caps=" + (info.getCapabilities() != null) + " status=" + info.getStatus());
    236         }
    237 
    238         if (!isHandledByOtherService(localPrinter)) {
    239             addPrinters(Collections.singletonList(info));
    240         }
    241     }
    242 
    243     /**
    244      * Return true if the {@link PrinterId} corresponds to a high-priority printer
    245      */
    246     boolean isPriority(PrinterId printerId) {
    247         return mTrackingIds.contains(printerId);
    248     }
    249 
    250     /**
    251      * Return true if the {@link PrinterId} corresponds to a known printer
    252      */
    253     boolean isKnown(PrinterId printerId) {
    254         return mPrinters.containsKey(printerId);
    255     }
    256 
    257     /**
    258      * Load "known good" printer IDs from storage, if possible
    259      */
    260     private void loadKnownGood() {
    261         File file = new File(mPrintService.getCacheDir(), KNOWN_GOOD_FILE);
    262         if (!file.exists()) return;
    263         try (JsonReader reader = new JsonReader(new FileReader(file))) {
    264             reader.beginArray();
    265             while (reader.hasNext()) {
    266                 String localId = reader.nextString();
    267                 mKnownGood.add(mPrintService.generatePrinterId(localId));
    268             }
    269             reader.endArray();
    270         } catch (IOException e) {
    271             Log.w(TAG, "Failed to read known good list", e);
    272         }
    273     }
    274 
    275     /**
    276      * Save "known good" printer IDs to storage, if possible
    277      */
    278     private void saveKnownGood() {
    279         File file = new File(mPrintService.getCacheDir(), KNOWN_GOOD_FILE);
    280         try (JsonWriter writer = new JsonWriter(new FileWriter(file))) {
    281             writer.beginArray();
    282             for (int i = 0; i < Math.min(KNOWN_GOOD_MAX, mKnownGood.size()); i++) {
    283                 writer.value(mKnownGood.get(i).getLocalId());
    284             }
    285             writer.endArray();
    286         } catch (IOException e) {
    287             Log.w(TAG, "Failed to write known good list", e);
    288         }
    289     }
    290 
    291     /**
    292      * Is this printer handled by another print service and should be suppressed?
    293      *
    294      * @param printer The printer that might need to be suppressed
    295      *
    296      * @return {@code true} iff the printer should be suppressed
    297      */
    298     private boolean isHandledByOtherService(LocalPrinter printer) {
    299         InetAddress address = printer.getAddress();
    300         if (address == null) return false;
    301 
    302         ArrayList<String> printerServices = mPrintersOfOtherService.get(printer.getAddress());
    303 
    304         if (printerServices != null) {
    305             int numServices = printerServices.size();
    306             for (int i = 0; i < numServices; i++) {
    307                 if (mEnabledServices.contains(printerServices.get(i))) {
    308                     return true;
    309                 }
    310             }
    311         }
    312 
    313         return false;
    314     }
    315 
    316     /**
    317      * If the system's print service state changed some printer might be newly suppressed or not
    318      * suppressed anymore.
    319      */
    320     private void onPrintServicesStateUpdated() {
    321         ArrayList<PrinterInfo> printersToAdd = new ArrayList<>();
    322         ArrayList<PrinterId> printersToRemove = new ArrayList<>();
    323         for (LocalPrinter printer : mPrinters.values()) {
    324             PrinterInfo info = printer.createPrinterInfo();
    325 
    326             if (printer.getCapabilities() != null && printer.isFound()
    327                     && !isHandledByOtherService(printer) && info != null) {
    328                 printersToAdd.add(info);
    329             } else {
    330                 printersToRemove.add(printer.getPrinterId());
    331             }
    332         }
    333 
    334         removePrinters(printersToRemove);
    335         addPrinters(printersToAdd);
    336     }
    337 
    338     @Override
    339     public void onPrintServiceRecommendationsChanged() {
    340         mPrintersOfOtherService.clear();
    341 
    342         List<RecommendationInfo> infos = mPrintManager.getPrintServiceRecommendations();
    343 
    344         int numInfos = infos.size();
    345         for (int i = 0; i < numInfos; i++) {
    346             RecommendationInfo info = infos.get(i);
    347             String packageName = info.getPackageName().toString();
    348 
    349             if (!packageName.equals(mPrintService.getPackageName())) {
    350                 for (InetAddress address : info.getDiscoveredPrinters()) {
    351                     ArrayList<String> services = mPrintersOfOtherService.get(address);
    352 
    353                     if (services == null) {
    354                         services = new ArrayList<>(1);
    355                         mPrintersOfOtherService.put(address, services);
    356                     }
    357 
    358                     services.add(packageName);
    359                 }
    360             }
    361         }
    362 
    363         onPrintServicesStateUpdated();
    364     }
    365 
    366     @Override
    367     public void onPrintServicesChanged() {
    368         mEnabledServices.clear();
    369 
    370         List<PrintServiceInfo> infos = mPrintManager.getPrintServices(
    371                 PrintManager.ENABLED_SERVICES);
    372 
    373         int numInfos = infos.size();
    374         for (int i = 0; i < numInfos; i++) {
    375             PrintServiceInfo info = infos.get(i);
    376             String packageName = info.getComponentName().getPackageName();
    377 
    378             if (!packageName.equals(mPrintService.getPackageName())) {
    379                 mEnabledServices.add(packageName);
    380             }
    381         }
    382 
    383         onPrintServicesStateUpdated();
    384     }
    385 
    386     /** A runnable that periodically removes expired printers, when any exist */
    387     private class ExpirePrinters implements Runnable {
    388         @Override
    389         public void run() {
    390             boolean allFound = true;
    391             List<PrinterId> idsToRemove = new ArrayList<>();
    392 
    393             for (LocalPrinter localPrinter : mPrinters.values()) {
    394                 if (localPrinter.isExpired()) {
    395                     if (DEBUG) Log.d(TAG, "Expiring " + localPrinter);
    396                     idsToRemove.add(localPrinter.getPrinterId());
    397                 }
    398                 if (!localPrinter.isFound()) allFound = false;
    399             }
    400             idsToRemove.forEach(mPrinters::remove);
    401             removePrinters(idsToRemove);
    402             if (!allFound) {
    403                 mPrintService.getMainHandler().postDelayed(this, PRINTER_EXPIRATION_MILLIS);
    404             } else {
    405                 mExpirePrinters = null;
    406             }
    407         }
    408     }
    409 }