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