• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
CapabilitiesCache(BuiltInPrintService service, Backend backend, int maxConcurrent)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
close()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 */
onCapabilities(LocalPrinterCapabilities capabilities)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      */
request(DiscoveredPrinter printer, boolean highPriority, OnLocalPrinterCapabilities onLocalPrinterCapabilities)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      */
get(DiscoveredPrinter printer)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      */
cancel(OnLocalPrinterCapabilities onLocalPrinterCapabilities)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 */
startNextRequest()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 */
getNextRequest()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 
Request(DiscoveredPrinter printer, long timeout)237         Request(DiscoveredPrinter printer, long timeout) {
238             mPrinter = printer;
239             mTimeout = timeout;
240         }
241 
start()242         private void start() {
243             mQuery = mBackend.getCapabilities(mPrinter.path, mTimeout, mHighPriority, this);
244         }
245 
cancel()246         private void cancel() {
247             if (mQuery != null) {
248                 mQuery.forceCancel();
249                 mQuery = null;
250             }
251         }
252 
253         @Override
accept(LocalPrinterCapabilities capabilities)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