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