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      */
CapabilitiesCache(Context context, Backend backend, int maxConcurrent)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
close()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      */
evictOnNetworkChange(Uri printerUri)89     public void evictOnNetworkChange(Uri printerUri) {
90         mToEvict.add(printerUri);
91     }
92 
93     /** Callback for receiving capabilities */
94     public interface OnLocalPrinterCapabilities {
onCapabilities(LocalPrinterCapabilities capabilities)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      */
request(DiscoveredPrinter printer, boolean highPriority, OnLocalPrinterCapabilities onLocalPrinterCapabilities)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 */
startNextRequest()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 */
getNextRequest()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 
Request(DiscoveredPrinter printer)211         Request(DiscoveredPrinter printer) {
212             this.printer = printer;
213         }
214     }
215 }