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