Home | History | Annotate | Download | only in ipp
      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.ipp;
     19 
     20 import android.content.Context;
     21 import android.net.Uri;
     22 import android.os.AsyncTask;
     23 import android.text.TextUtils;
     24 import android.util.Log;
     25 import android.util.LruCache;
     26 
     27 import com.android.bips.discovery.DiscoveredPrinter;
     28 import com.android.bips.jni.LocalPrinterCapabilities;
     29 import com.android.bips.util.WifiMonitor;
     30 
     31 import java.util.ArrayList;
     32 import java.util.HashMap;
     33 import java.util.HashSet;
     34 import java.util.List;
     35 import java.util.Map;
     36 import java.util.Set;
     37 
     38 /**
     39  * A cache of printer URIs (see {@link DiscoveredPrinter#getUri}) to printer capabilities,
     40  * with the ability to fetch them on cache misses. {@link #close} must be called when use
     41  * is complete..
     42  */
     43 public class CapabilitiesCache extends LruCache<Uri, LocalPrinterCapabilities> implements
     44         AutoCloseable {
     45     private static final String TAG = CapabilitiesCache.class.getSimpleName();
     46     private static final boolean DEBUG = false;
     47 
     48     // Maximum number of capability queries to perform at any one time, so as not to overwhelm
     49     // AsyncTask.THREAD_POOL_EXECUTOR
     50     public static final int DEFAULT_MAX_CONCURRENT = 3;
     51 
     52     // Maximum number of printers expected on a single network
     53     private static final int CACHE_SIZE = 100;
     54 
     55     // Maximum time per retry before giving up on first pass
     56     private static final int FIRST_PASS_TIMEOUT = 500;
     57 
     58     // Maximum time per retry before giving up on second pass. Must differ from FIRST_PASS_TIMEOUT.
     59     private static final int SECOND_PASS_TIMEOUT = 8000;
     60 
     61     private final Map<Uri, Request> mRequests = new HashMap<>();
     62     private final Set<Uri> mToEvict = new HashSet<>();
     63     private final int mMaxConcurrent;
     64     private final Backend mBackend;
     65     private final WifiMonitor mWifiMonitor;
     66     private boolean mClosed = false;
     67 
     68     /**
     69      * @param maxConcurrent Maximum number of capabilities requests to make at any one time
     70      */
     71     public CapabilitiesCache(Context context, Backend backend, int maxConcurrent) {
     72         super(CACHE_SIZE);
     73         if (DEBUG) Log.d(TAG, "CapabilitiesCache()");
     74 
     75         mBackend = backend;
     76         mMaxConcurrent = maxConcurrent;
     77         mWifiMonitor = new WifiMonitor(context, connected -> {
     78             if (!connected) {
     79                 // Evict specified device capabilities when network is lost.
     80                 if (DEBUG) Log.d(TAG, "Evicting " + mToEvict);
     81                 mToEvict.forEach(this::remove);
     82                 mToEvict.clear();
     83             }
     84         });
     85     }
     86 
     87     @Override
     88     public void close() {
     89         if (DEBUG) Log.d(TAG, "close()");
     90         mClosed = true;
     91         mWifiMonitor.close();
     92     }
     93 
     94     /**
     95      * Indicate that a device should be evicted when this object is closed or network
     96      * parameters change.
     97      */
     98     public void evictOnNetworkChange(Uri printerUri) {
     99         mToEvict.add(printerUri);
    100     }
    101 
    102     /** Callback for receiving capabilities */
    103     public interface OnLocalPrinterCapabilities {
    104         void onCapabilities(DiscoveredPrinter printer, LocalPrinterCapabilities capabilities);
    105     }
    106 
    107     /**
    108      * Query capabilities and return full results to the listener. A full result includes
    109      * enough backend data and is suitable for printing. If full data is already available
    110      * it will be returned to the callback immediately.
    111      *
    112      * @param highPriority if true, perform this query before others
    113      * @param onLocalPrinterCapabilities listener to receive capabilities. Receives null
    114      *                                   if the attempt fails
    115      */
    116     public void request(DiscoveredPrinter printer, boolean highPriority,
    117             OnLocalPrinterCapabilities onLocalPrinterCapabilities) {
    118         if (DEBUG) Log.d(TAG, "request() printer=" + printer + " high=" + highPriority);
    119 
    120         Uri printerUri = printer.getUri();
    121         Uri printerPath = printer.path;
    122         LocalPrinterCapabilities capabilities = get(printer.getUri());
    123         if (capabilities != null && capabilities.nativeData != null) {
    124             onLocalPrinterCapabilities.onCapabilities(printer, capabilities);
    125             return;
    126         }
    127 
    128         Request request = mRequests.get(printerUri);
    129         if (request == null) {
    130             if (highPriority) {
    131                 // Go straight to the long-timeout request
    132                 request = new Request(printer, SECOND_PASS_TIMEOUT);
    133             } else {
    134                 request = new Request(printer, FIRST_PASS_TIMEOUT);
    135             }
    136             mRequests.put(printerUri, request);
    137         } else if (!request.printer.path.equals(printerPath)) {
    138             Log.w(TAG, "Capabilities request for printer " + printer +
    139                     " overlaps with different path " + request.printer.path);
    140             onLocalPrinterCapabilities.onCapabilities(printer, null);
    141             return;
    142         }
    143 
    144         request.callbacks.add(onLocalPrinterCapabilities);
    145 
    146         if (highPriority) {
    147             request.highPriority = true;
    148         }
    149 
    150         startNextRequest();
    151     }
    152 
    153     /**
    154      * Cancel any outstanding attempts to get capabilities on this callback
    155      */
    156     public void cancel(OnLocalPrinterCapabilities onLocalPrinterCapabilities) {
    157         List<Uri> toDrop = new ArrayList<>();
    158         for (Map.Entry<Uri, Request> entry : mRequests.entrySet()) {
    159             Request request = entry.getValue();
    160             request.callbacks.remove(onLocalPrinterCapabilities);
    161             if (request.callbacks.isEmpty()) {
    162                 // There is no further interest in this request so cancel it
    163                 toDrop.add(entry.getKey());
    164                 if (request.query != null) {
    165                     request.query.cancel(true);
    166                 }
    167             }
    168         }
    169         toDrop.forEach(mRequests::remove);
    170     }
    171 
    172     /** Look for next query and launch it */
    173     private void startNextRequest() {
    174         final Request request = getNextRequest();
    175         if (request == null) return;
    176 
    177         request.query = mBackend.getCapabilities(request.printer.path, request.timeout, capabilities -> {
    178             DiscoveredPrinter printer = request.printer;
    179             if (DEBUG) Log.d(TAG, "Capabilities for " + printer + " cap=" + capabilities);
    180 
    181             if (mClosed) return;
    182             mRequests.remove(printer.getUri());
    183 
    184             // Grab uuid from capabilities if possible
    185             Uri capUuid = null;
    186             if (capabilities != null) {
    187                 if (!TextUtils.isEmpty(capabilities.uuid)) {
    188                     capUuid = Uri.parse(capabilities.uuid);
    189                 }
    190                 if (printer.uuid != null && !printer.uuid.equals(capUuid)) {
    191                     Log.w(TAG, "UUID mismatch for " + printer + "; rejecting capabilities");
    192                     capabilities = null;
    193                 }
    194             }
    195 
    196             if (capabilities == null) {
    197                 if (request.timeout == FIRST_PASS_TIMEOUT) {
    198                     // Printer did not respond quickly, try again in the slow lane
    199                     request.timeout = SECOND_PASS_TIMEOUT;
    200                     request.query = null;
    201                     mRequests.put(printer.getUri(), request);
    202                     startNextRequest();
    203                     return;
    204                 } else {
    205                     remove(printer.getUri());
    206                 }
    207             } else {
    208                 Uri key = printer.getUri();
    209                 if (printer.uuid == null) {
    210                     // For non-uuid URIs, evict later
    211                     evictOnNetworkChange(key);
    212                     if (capUuid != null) {
    213                         // Upgrade to UUID if we have it
    214                         key = capUuid;
    215                     }
    216                 }
    217                 put(key, capabilities);
    218             }
    219 
    220             for (OnLocalPrinterCapabilities callback : request.callbacks) {
    221                 callback.onCapabilities(printer, capabilities);
    222             }
    223             startNextRequest();
    224         });
    225     }
    226 
    227     /** Return the next request if it is appropriate to perform one */
    228     private Request getNextRequest() {
    229         Request found = null;
    230         int total = 0;
    231         for (Request request : mRequests.values()) {
    232             if (request.query != null) {
    233                 total++;
    234             } else if (found == null || (!found.highPriority && request.highPriority) ||
    235                     (found.highPriority == request.highPriority && request.timeout < found.timeout)) {
    236                 // First valid or higher priority request
    237                 found = request;
    238             }
    239         }
    240 
    241         if (total >= mMaxConcurrent) return null;
    242 
    243         return found;
    244     }
    245 
    246     /** Holds an outstanding capabilities request */
    247     public class Request {
    248         final DiscoveredPrinter printer;
    249         final Set<OnLocalPrinterCapabilities> callbacks = new HashSet<>();
    250         AsyncTask<?, ?, ?> query;
    251         boolean highPriority = false;
    252         long timeout;
    253 
    254         Request(DiscoveredPrinter printer, long timeout) {
    255             this.printer = printer;
    256             this.timeout = timeout;
    257         }
    258     }
    259 }