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