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.os.AsyncTask; 23 import android.text.TextUtils; 24 import android.util.Log; 25 import android.util.LruCache; 26 27 import com.android.bips.discovery.DiscoveredPrinter; 28 import com.android.bips.jni.LocalPrinterCapabilities; 29 import com.android.bips.util.WifiMonitor; 30 31 import java.util.ArrayList; 32 import java.util.HashMap; 33 import java.util.HashSet; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Set; 37 38 /** 39 * A cache of printer URIs (see {@link DiscoveredPrinter#getUri}) to printer capabilities, 40 * with the ability to fetch them on cache misses. {@link #close} must be called when use 41 * is complete.. 42 */ 43 public class CapabilitiesCache extends LruCache<Uri, LocalPrinterCapabilities> implements 44 AutoCloseable { 45 private static final String TAG = CapabilitiesCache.class.getSimpleName(); 46 private static final boolean DEBUG = false; 47 48 // Maximum number of capability queries to perform at any one time, so as not to overwhelm 49 // AsyncTask.THREAD_POOL_EXECUTOR 50 public static final int DEFAULT_MAX_CONCURRENT = 3; 51 52 // Maximum number of printers expected on a single network 53 private static final int CACHE_SIZE = 100; 54 55 // Maximum time per retry before giving up on first pass 56 private static final int FIRST_PASS_TIMEOUT = 500; 57 58 // Maximum time per retry before giving up on second pass. Must differ from FIRST_PASS_TIMEOUT. 59 private static final int SECOND_PASS_TIMEOUT = 8000; 60 61 private final Map<Uri, Request> mRequests = new HashMap<>(); 62 private final Set<Uri> mToEvict = new HashSet<>(); 63 private final int mMaxConcurrent; 64 private final Backend mBackend; 65 private final WifiMonitor mWifiMonitor; 66 private boolean mClosed = false; 67 68 /** 69 * @param maxConcurrent Maximum number of capabilities requests to make at any one time 70 */ 71 public CapabilitiesCache(Context context, Backend backend, int maxConcurrent) { 72 super(CACHE_SIZE); 73 if (DEBUG) Log.d(TAG, "CapabilitiesCache()"); 74 75 mBackend = backend; 76 mMaxConcurrent = maxConcurrent; 77 mWifiMonitor = new WifiMonitor(context, connected -> { 78 if (!connected) { 79 // Evict specified device capabilities when network is lost. 80 if (DEBUG) Log.d(TAG, "Evicting " + mToEvict); 81 mToEvict.forEach(this::remove); 82 mToEvict.clear(); 83 } 84 }); 85 } 86 87 @Override 88 public void close() { 89 if (DEBUG) Log.d(TAG, "close()"); 90 mClosed = true; 91 mWifiMonitor.close(); 92 } 93 94 /** 95 * Indicate that a device should be evicted when this object is closed or network 96 * parameters change. 97 */ 98 public void evictOnNetworkChange(Uri printerUri) { 99 mToEvict.add(printerUri); 100 } 101 102 /** Callback for receiving capabilities */ 103 public interface OnLocalPrinterCapabilities { 104 void onCapabilities(DiscoveredPrinter printer, LocalPrinterCapabilities capabilities); 105 } 106 107 /** 108 * Query capabilities and return full results to the listener. A full result includes 109 * enough backend data and is suitable for printing. If full data is already available 110 * it will be returned to the callback immediately. 111 * 112 * @param highPriority if true, perform this query before others 113 * @param onLocalPrinterCapabilities listener to receive capabilities. Receives null 114 * if the attempt fails 115 */ 116 public void request(DiscoveredPrinter printer, boolean highPriority, 117 OnLocalPrinterCapabilities onLocalPrinterCapabilities) { 118 if (DEBUG) Log.d(TAG, "request() printer=" + printer + " high=" + highPriority); 119 120 Uri printerUri = printer.getUri(); 121 Uri printerPath = printer.path; 122 LocalPrinterCapabilities capabilities = get(printer.getUri()); 123 if (capabilities != null && capabilities.nativeData != null) { 124 onLocalPrinterCapabilities.onCapabilities(printer, capabilities); 125 return; 126 } 127 128 Request request = mRequests.get(printerUri); 129 if (request == null) { 130 if (highPriority) { 131 // Go straight to the long-timeout request 132 request = new Request(printer, SECOND_PASS_TIMEOUT); 133 } else { 134 request = new Request(printer, FIRST_PASS_TIMEOUT); 135 } 136 mRequests.put(printerUri, request); 137 } else if (!request.printer.path.equals(printerPath)) { 138 Log.w(TAG, "Capabilities request for printer " + printer + 139 " overlaps with different path " + request.printer.path); 140 onLocalPrinterCapabilities.onCapabilities(printer, null); 141 return; 142 } 143 144 request.callbacks.add(onLocalPrinterCapabilities); 145 146 if (highPriority) { 147 request.highPriority = true; 148 } 149 150 startNextRequest(); 151 } 152 153 /** 154 * Cancel any outstanding attempts to get capabilities on this callback 155 */ 156 public void cancel(OnLocalPrinterCapabilities onLocalPrinterCapabilities) { 157 List<Uri> toDrop = new ArrayList<>(); 158 for (Map.Entry<Uri, Request> entry : mRequests.entrySet()) { 159 Request request = entry.getValue(); 160 request.callbacks.remove(onLocalPrinterCapabilities); 161 if (request.callbacks.isEmpty()) { 162 // There is no further interest in this request so cancel it 163 toDrop.add(entry.getKey()); 164 if (request.query != null) { 165 request.query.cancel(true); 166 } 167 } 168 } 169 toDrop.forEach(mRequests::remove); 170 } 171 172 /** Look for next query and launch it */ 173 private void startNextRequest() { 174 final Request request = getNextRequest(); 175 if (request == null) return; 176 177 request.query = mBackend.getCapabilities(request.printer.path, request.timeout, capabilities -> { 178 DiscoveredPrinter printer = request.printer; 179 if (DEBUG) Log.d(TAG, "Capabilities for " + printer + " cap=" + capabilities); 180 181 if (mClosed) return; 182 mRequests.remove(printer.getUri()); 183 184 // Grab uuid from capabilities if possible 185 Uri capUuid = null; 186 if (capabilities != null) { 187 if (!TextUtils.isEmpty(capabilities.uuid)) { 188 capUuid = Uri.parse(capabilities.uuid); 189 } 190 if (printer.uuid != null && !printer.uuid.equals(capUuid)) { 191 Log.w(TAG, "UUID mismatch for " + printer + "; rejecting capabilities"); 192 capabilities = null; 193 } 194 } 195 196 if (capabilities == null) { 197 if (request.timeout == FIRST_PASS_TIMEOUT) { 198 // Printer did not respond quickly, try again in the slow lane 199 request.timeout = SECOND_PASS_TIMEOUT; 200 request.query = null; 201 mRequests.put(printer.getUri(), request); 202 startNextRequest(); 203 return; 204 } else { 205 remove(printer.getUri()); 206 } 207 } else { 208 Uri key = printer.getUri(); 209 if (printer.uuid == null) { 210 // For non-uuid URIs, evict later 211 evictOnNetworkChange(key); 212 if (capUuid != null) { 213 // Upgrade to UUID if we have it 214 key = capUuid; 215 } 216 } 217 put(key, capabilities); 218 } 219 220 for (OnLocalPrinterCapabilities callback : request.callbacks) { 221 callback.onCapabilities(printer, capabilities); 222 } 223 startNextRequest(); 224 }); 225 } 226 227 /** Return the next request if it is appropriate to perform one */ 228 private Request getNextRequest() { 229 Request found = null; 230 int total = 0; 231 for (Request request : mRequests.values()) { 232 if (request.query != null) { 233 total++; 234 } else if (found == null || (!found.highPriority && request.highPriority) || 235 (found.highPriority == request.highPriority && request.timeout < found.timeout)) { 236 // First valid or higher priority request 237 found = request; 238 } 239 } 240 241 if (total >= mMaxConcurrent) return null; 242 243 return found; 244 } 245 246 /** Holds an outstanding capabilities request */ 247 public class Request { 248 final DiscoveredPrinter printer; 249 final Set<OnLocalPrinterCapabilities> callbacks = new HashSet<>(); 250 AsyncTask<?, ?, ?> query; 251 boolean highPriority = false; 252 long timeout; 253 254 Request(DiscoveredPrinter printer, long timeout) { 255 this.printer = printer; 256 this.timeout = timeout; 257 } 258 } 259 }