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         if (DEBUG) {
    227             Log.d(TAG, "handlePrinter: reporting " + localPrinter +
    228                     " caps=" + (info.getCapabilities() != null) + " status=" + info.getStatus());
    229         }
    230 
    231         if (!isHandledByOtherService(localPrinter)) {
    232             addPrinters(Collections.singletonList(info));
    233         }
    234     }
    235 
    236     /**
    237      * Return true if the {@link PrinterId} corresponds to a high-priority printer
    238      */
    239     boolean isPriority(PrinterId printerId) {
    240         return mPriorityIds.contains(printerId) || mTrackingIds.contains(printerId);
    241     }
    242 
    243     /**
    244      * Return true if the {@link PrinterId} corresponds to a known printer
    245      */
    246     boolean isKnown(PrinterId printerId) {
    247         return mPrinters.containsKey(printerId);
    248     }
    249 
    250     /**
    251      * Load "known good" printer IDs from storage, if possible
    252      */
    253     private void loadKnownGood() {
    254         File file = new File(mPrintService.getCacheDir(), KNOWN_GOOD_FILE);
    255         if (!file.exists()) return;
    256         try (JsonReader reader = new JsonReader(new FileReader(file))) {
    257             reader.beginArray();
    258             while (reader.hasNext()) {
    259                 String localId = reader.nextString();
    260                 mKnownGood.add(mPrintService.generatePrinterId(localId));
    261             }
    262             reader.endArray();
    263         } catch (IOException e) {
    264             Log.w(TAG, "Failed to read known good list", e);
    265         }
    266     }
    267 
    268     /**
    269      * Save "known good" printer IDs to storage, if possible
    270      */
    271     private void saveKnownGood() {
    272         File file = new File(mPrintService.getCacheDir(), KNOWN_GOOD_FILE);
    273         try (JsonWriter writer = new JsonWriter(new FileWriter(file))) {
    274             writer.beginArray();
    275             for (int i = 0; i < Math.min(KNOWN_GOOD_MAX, mKnownGood.size()); i++) {
    276                 writer.value(mKnownGood.get(i).getLocalId());
    277             }
    278             writer.endArray();
    279         } catch (IOException e) {
    280             Log.w(TAG, "Failed to write known good list", e);
    281         }
    282     }
    283 
    284     /**
    285      * Is this printer handled by another print service and should be suppressed?
    286      *
    287      * @param printer The printer that might need to be suppressed
    288      *
    289      * @return {@code true} iff the printer should be suppressed
    290      */
    291     private boolean isHandledByOtherService(LocalPrinter printer) {
    292         ArrayList<String> printerServices;
    293         try {
    294             printerServices = mPrintersOfOtherService.get(printer.getAddress());
    295         } catch (UnknownHostException e) {
    296             Log.e(TAG, "Cannot resolve address for " + printer, e);
    297             return false;
    298         }
    299 
    300         if (printerServices != null) {
    301             int numServices = printerServices.size();
    302             for (int i = 0; i < numServices; i++) {
    303                 if (mEnabledServices.contains(printerServices.get(i))) {
    304                     return true;
    305                 }
    306             }
    307         }
    308 
    309         return false;
    310     }
    311 
    312     /**
    313      * If the system's print service state changed some printer might be newly suppressed or not
    314      * suppressed anymore.
    315      */
    316     private void onPrintServicesStateUpdated() {
    317         ArrayList<PrinterInfo> printersToAdd = new ArrayList<>();
    318         ArrayList<PrinterId> printersToRemove = new ArrayList<>();
    319         for (LocalPrinter printer : mPrinters.values()) {
    320             PrinterInfo info = printer.createPrinterInfo();
    321 
    322             if (printer.getCapabilities() != null && printer.isFound()
    323                     && !isHandledByOtherService(printer) && info != null) {
    324                 printersToAdd.add(info);
    325             } else {
    326                 printersToRemove.add(printer.getPrinterId());
    327             }
    328         }
    329 
    330         removePrinters(printersToRemove);
    331         addPrinters(printersToAdd);
    332     }
    333 
    334     @Override
    335     public void onPrintServiceRecommendationsChanged() {
    336         mPrintersOfOtherService.clear();
    337 
    338         List<RecommendationInfo> infos = mPrintManager.getPrintServiceRecommendations();
    339 
    340         int numInfos = infos.size();
    341         for (int i = 0; i < numInfos; i++) {
    342             RecommendationInfo info = infos.get(i);
    343             String packageName = info.getPackageName().toString();
    344 
    345             if (!packageName.equals(mPrintService.getPackageName())) {
    346                 for (InetAddress address : info.getDiscoveredPrinters()) {
    347                     ArrayList<String> services = mPrintersOfOtherService.get(address);
    348 
    349                     if (services == null) {
    350                         services = new ArrayList<>(1);
    351                         mPrintersOfOtherService.put(address, services);
    352                     }
    353 
    354                     services.add(packageName);
    355                 }
    356             }
    357         }
    358 
    359         onPrintServicesStateUpdated();
    360     }
    361 
    362     @Override
    363     public void onPrintServicesChanged() {
    364         mEnabledServices.clear();
    365 
    366         List<PrintServiceInfo> infos = mPrintManager.getPrintServices(
    367                 PrintManager.ENABLED_SERVICES);
    368 
    369         int numInfos = infos.size();
    370         for (int i = 0; i < numInfos; i++) {
    371             PrintServiceInfo info = infos.get(i);
    372             String packageName = info.getComponentName().getPackageName();
    373 
    374             if (!packageName.equals(mPrintService.getPackageName())) {
    375                 mEnabledServices.add(packageName);
    376             }
    377         }
    378 
    379         onPrintServicesStateUpdated();
    380     }
    381 
    382     /** A runnable that periodically removes expired printers, when any exist */
    383     private class ExpirePrinters implements Runnable {
    384         @Override
    385         public void run() {
    386             boolean allFound = true;
    387             List<PrinterId> idsToRemove = new ArrayList<>();
    388 
    389             for (LocalPrinter localPrinter : mPrinters.values()) {
    390                 if (localPrinter.isExpired()) {
    391                     if (DEBUG) Log.d(TAG, "Expiring " + localPrinter);
    392                     idsToRemove.add(localPrinter.getPrinterId());
    393                 }
    394                 if (!localPrinter.isFound()) allFound = false;
    395             }
    396             idsToRemove.forEach(mPrinters::remove);
    397             removePrinters(idsToRemove);
    398             if (!allFound) {
    399                 mPrintService.getMainHandler().postDelayed(this, PRINTER_EXPIRATION_MILLIS);
    400             } else {
    401                 mExpirePrinters = null;
    402             }
    403         }
    404     }
    405 }