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