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.BroadcastReceiver;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.net.NetworkInfo;
     24 import android.net.Uri;
     25 import android.net.wifi.p2p.WifiP2pManager;
     26 import android.os.AsyncTask;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 import android.util.LruCache;
     30 
     31 import com.android.bips.BuiltInPrintService;
     32 import com.android.bips.discovery.DiscoveredPrinter;
     33 import com.android.bips.jni.LocalPrinterCapabilities;
     34 import com.android.bips.p2p.P2pUtils;
     35 import com.android.bips.util.BroadcastMonitor;
     36 import com.android.bips.util.WifiMonitor;
     37 
     38 import java.util.ArrayList;
     39 import java.util.HashMap;
     40 import java.util.HashSet;
     41 import java.util.List;
     42 import java.util.Map;
     43 import java.util.Set;
     44 import java.util.function.Consumer;
     45 
     46 /**
     47  * A cache of printer URIs (see {@link DiscoveredPrinter#path}) to printer capabilities,
     48  * with the ability to fetch them on cache misses. {@link #close} must be called when use
     49  * is complete.
     50  */
     51 public class CapabilitiesCache extends LruCache<Uri, LocalPrinterCapabilities> implements
     52         AutoCloseable {
     53     private static final String TAG = CapabilitiesCache.class.getSimpleName();
     54     private static final boolean DEBUG = false;
     55 
     56     // Maximum number of capability queries to perform at any one time, so as not to overwhelm
     57     // AsyncTask.THREAD_POOL_EXECUTOR
     58     public static final int DEFAULT_MAX_CONCURRENT = 3;
     59 
     60     // Maximum number of printers expected on a single network
     61     private static final int CACHE_SIZE = 100;
     62 
     63     // Maximum time per retry before giving up on first pass
     64     private static final int FIRST_PASS_TIMEOUT = 500;
     65 
     66     // Maximum time per retry before giving up on second pass. Must differ from FIRST_PASS_TIMEOUT.
     67     private static final int SECOND_PASS_TIMEOUT = 8000;
     68 
     69     // Outstanding requests based on printer path
     70     private final Map<Uri, Request> mRequests = new HashMap<>();
     71     private final Set<Uri> mToEvict = new HashSet<>();
     72     private final Set<Uri> mToEvictP2p = new HashSet<>();
     73     private final int mMaxConcurrent;
     74     private final Backend mBackend;
     75     private final WifiMonitor mWifiMonitor;
     76     private final BroadcastMonitor mP2pMonitor;
     77     private final BuiltInPrintService mService;
     78     private boolean mIsStopped = false;
     79 
     80     /**
     81      * @param maxConcurrent Maximum number of capabilities requests to make at any one time
     82      */
     83     public CapabilitiesCache(BuiltInPrintService service, Backend backend, int maxConcurrent) {
     84         super(CACHE_SIZE);
     85         if (DEBUG) Log.d(TAG, "CapabilitiesCache()");
     86 
     87         mService = service;
     88         mBackend = backend;
     89         mMaxConcurrent = maxConcurrent;
     90 
     91         mP2pMonitor = mService.receiveBroadcasts(new BroadcastReceiver() {
     92             @Override
     93             public void onReceive(Context context, Intent intent) {
     94                 NetworkInfo info = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
     95                 if (!info.isConnected()) {
     96                     // Evict specified device capabilities when P2P network is lost.
     97                     if (DEBUG) Log.d(TAG, "Evicting P2P " + mToEvictP2p);
     98                     for (Uri uri : mToEvictP2p) {
     99                         remove(uri);
    100                     }
    101                     mToEvictP2p.clear();
    102                 }
    103             }
    104         }, WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
    105 
    106         mWifiMonitor = new WifiMonitor(service, connected -> {
    107             if (!connected) {
    108                 // Evict specified device capabilities when network is lost.
    109                 if (DEBUG) Log.d(TAG, "Evicting Wi-Fi " + mToEvict);
    110                 for (Uri uri : mToEvict) {
    111                     remove(uri);
    112                 }
    113                 mToEvict.clear();
    114             }
    115         });
    116     }
    117 
    118     @Override
    119     public void close() {
    120         if (DEBUG) Log.d(TAG, "stop()");
    121         mIsStopped = true;
    122         mWifiMonitor.close();
    123         mP2pMonitor.close();
    124     }
    125 
    126     /** Callback for receiving capabilities */
    127     public interface OnLocalPrinterCapabilities {
    128         /** Called when capabilities are retrieved */
    129         void onCapabilities(LocalPrinterCapabilities capabilities);
    130     }
    131 
    132     /**
    133      * Query capabilities and return full results to the listener. A full result includes
    134      * enough backend data and is suitable for printing. If full data is already available
    135      * it will be returned to the callback immediately.
    136      *
    137      * @param highPriority if true, perform this query before others
    138      * @param onLocalPrinterCapabilities listener to receive capabilities. Receives null
    139      *                                   if the attempt fails
    140      */
    141     public void request(DiscoveredPrinter printer, boolean highPriority,
    142             OnLocalPrinterCapabilities onLocalPrinterCapabilities) {
    143         if (DEBUG) Log.d(TAG, "request() printer=" + printer + " high=" + highPriority);
    144 
    145         LocalPrinterCapabilities capabilities = get(printer);
    146         if (capabilities != null && capabilities.nativeData != null) {
    147             onLocalPrinterCapabilities.onCapabilities(capabilities);
    148             return;
    149         }
    150 
    151         if (P2pUtils.isOnConnectedInterface(mService, printer)) {
    152             if (DEBUG) Log.d(TAG, "Adding to P2P evict list: " + printer);
    153             mToEvictP2p.add(printer.path);
    154         } else {
    155             if (DEBUG) Log.d(TAG, "Adding to WLAN evict list: " + printer);
    156             mToEvict.add(printer.path);
    157         }
    158 
    159         // Create a new request with timeout based on priority
    160         Request request = mRequests.computeIfAbsent(printer.path, uri ->
    161                 new Request(printer, highPriority ? SECOND_PASS_TIMEOUT : FIRST_PASS_TIMEOUT));
    162 
    163         if (highPriority) {
    164             request.mHighPriority = true;
    165         }
    166 
    167         request.mCallbacks.add(onLocalPrinterCapabilities);
    168 
    169         startNextRequest();
    170     }
    171 
    172     /**
    173      * Returns capabilities for the specified printer, if known
    174      */
    175     public LocalPrinterCapabilities get(DiscoveredPrinter printer) {
    176         return get(printer.path);
    177     }
    178 
    179     /**
    180      * Cancel all outstanding attempts to get capabilities for this callback
    181      */
    182     public void cancel(OnLocalPrinterCapabilities onLocalPrinterCapabilities) {
    183         List<Uri> toDrop = new ArrayList<>();
    184         for (Map.Entry<Uri, Request> entry : mRequests.entrySet()) {
    185             Request request = entry.getValue();
    186             request.mCallbacks.remove(onLocalPrinterCapabilities);
    187             if (request.mCallbacks.isEmpty()) {
    188                 toDrop.add(entry.getKey());
    189                 request.cancel();
    190             }
    191         }
    192         for (Uri request : toDrop) {
    193             mRequests.remove(request);
    194         }
    195     }
    196 
    197     /** Look for next query and launch it */
    198     private void startNextRequest() {
    199         final Request request = getNextRequest();
    200         if (request == null) {
    201             return;
    202         }
    203 
    204         request.start();
    205     }
    206 
    207     /** Return the next request if it is appropriate to perform one */
    208     private Request getNextRequest() {
    209         Request found = null;
    210         int total = 0;
    211         for (Request request : mRequests.values()) {
    212             if (request.mQuery != null) {
    213                 total++;
    214             } else if (found == null || (!found.mHighPriority && request.mHighPriority)
    215                     || (found.mHighPriority == request.mHighPriority
    216                     && request.mTimeout < found.mTimeout)) {
    217                 // First valid or higher priority request
    218                 found = request;
    219             }
    220         }
    221 
    222         if (total >= mMaxConcurrent) {
    223             return null;
    224         }
    225 
    226         return found;
    227     }
    228 
    229     /** Holds an outstanding capabilities request */
    230     public class Request implements Consumer<LocalPrinterCapabilities> {
    231         final DiscoveredPrinter mPrinter;
    232         final List<OnLocalPrinterCapabilities> mCallbacks = new ArrayList<>();
    233         GetCapabilitiesTask mQuery;
    234         boolean mHighPriority = false;
    235         long mTimeout;
    236 
    237         Request(DiscoveredPrinter printer, long timeout) {
    238             mPrinter = printer;
    239             mTimeout = timeout;
    240         }
    241 
    242         private void start() {
    243             mQuery = mBackend.getCapabilities(mPrinter.path, mTimeout, mHighPriority, this);
    244         }
    245 
    246         private void cancel() {
    247             if (mQuery != null) {
    248                 mQuery.forceCancel();
    249                 mQuery = null;
    250             }
    251         }
    252 
    253         @Override
    254         public void accept(LocalPrinterCapabilities capabilities) {
    255             DiscoveredPrinter printer = mPrinter;
    256             if (DEBUG) Log.d(TAG, "Capabilities for " + printer + " cap=" + capabilities);
    257 
    258             if (mIsStopped) {
    259                 return;
    260             }
    261             mRequests.remove(printer.path);
    262 
    263             // Grab uuid from capabilities if possible
    264             Uri capUuid = null;
    265             if (capabilities != null) {
    266                 if (!TextUtils.isEmpty(capabilities.uuid)) {
    267                     capUuid = Uri.parse(capabilities.uuid);
    268                 }
    269                 if (printer.uuid != null && !printer.uuid.equals(capUuid)) {
    270                     Log.w(TAG, "UUID mismatch for " + printer + "; rejecting capabilities");
    271                     capabilities = null;
    272                 }
    273             }
    274 
    275             if (capabilities == null) {
    276                 if (mTimeout == FIRST_PASS_TIMEOUT) {
    277                     // Printer did not respond quickly, try again in the slow lane
    278                     mTimeout = SECOND_PASS_TIMEOUT;
    279                     mQuery = null;
    280                     mRequests.put(printer.path, this);
    281                     startNextRequest();
    282                     return;
    283                 } else {
    284                     remove(printer.getUri());
    285                 }
    286             } else {
    287                 put(printer.path, capabilities);
    288             }
    289 
    290             LocalPrinterCapabilities result = capabilities;
    291             for (OnLocalPrinterCapabilities callback : mCallbacks) {
    292                 callback.onCapabilities(result);
    293             }
    294             startNextRequest();
    295         }
    296     }
    297 }
    298