| /* | |
| * Copyright (C) 2016 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package com.android.bips.ipp; | |
| import android.content.BroadcastReceiver; | |
| import android.content.Context; | |
| import android.content.Intent; | |
| import android.net.NetworkInfo; | |
| import android.net.Uri; | |
| import android.net.wifi.p2p.WifiP2pManager; | |
| import android.text.TextUtils; | |
| import android.util.Log; | |
| import android.util.LruCache; | |
| import com.android.bips.BuiltInPrintService; | |
| import com.android.bips.discovery.DiscoveredPrinter; | |
| import com.android.bips.jni.LocalPrinterCapabilities; | |
| import com.android.bips.p2p.P2pUtils; | |
| import com.android.bips.util.BroadcastMonitor; | |
| import com.android.bips.util.WifiMonitor; | |
| import java.util.ArrayList; | |
| import java.util.HashMap; | |
| import java.util.HashSet; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import java.util.function.Consumer; | |
| /** | |
| * A cache of printer URIs (see {@link DiscoveredPrinter#path}) to printer capabilities, | |
| * with the ability to fetch them on cache misses. {@link #close} must be called when use | |
| * is complete. | |
| */ | |
| public class CapabilitiesCache implements AutoCloseable { | |
| private static final String TAG = CapabilitiesCache.class.getSimpleName(); | |
| private static final boolean DEBUG = false; | |
| // Maximum number of capability queries to perform at any one time, so as not to overwhelm | |
| // AsyncTask.THREAD_POOL_EXECUTOR | |
| public static final int DEFAULT_MAX_CONCURRENT = 3; | |
| // Maximum number of printers expected on a single network | |
| private static final int CACHE_SIZE = 100; | |
| // Maximum time per retry before giving up on first pass | |
| private static final int FIRST_PASS_TIMEOUT = 500; | |
| // Maximum time per retry before giving up on second pass. Must differ from FIRST_PASS_TIMEOUT. | |
| private static final int SECOND_PASS_TIMEOUT = 8000; | |
| // Underlying cache | |
| private final LruCache<Uri, LocalPrinterCapabilities> mCache = new LruCache<>(CACHE_SIZE); | |
| // Outstanding requests based on printer path | |
| private final Map<Uri, Request> mRequests = new HashMap<>(); | |
| private final Set<Uri> mToEvict = new HashSet<>(); | |
| private final Set<Uri> mToEvictP2p = new HashSet<>(); | |
| private final int mMaxConcurrent; | |
| private final Backend mBackend; | |
| private final WifiMonitor mWifiMonitor; | |
| private final BroadcastMonitor mP2pMonitor; | |
| private final BuiltInPrintService mService; | |
| private boolean mIsStopped = false; | |
| /** | |
| * @param maxConcurrent Maximum number of capabilities requests to make at any one time | |
| */ | |
| public CapabilitiesCache(BuiltInPrintService service, Backend backend, int maxConcurrent) { | |
| if (DEBUG) Log.d(TAG, "CapabilitiesCache()"); | |
| mService = service; | |
| mBackend = backend; | |
| mMaxConcurrent = maxConcurrent; | |
| mP2pMonitor = mService.receiveBroadcasts(new BroadcastReceiver() { | |
| @Override | |
| public void onReceive(Context context, Intent intent) { | |
| NetworkInfo info = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO); | |
| if (!info.isConnected()) { | |
| // Evict specified device capabilities when P2P network is lost. | |
| if (DEBUG) Log.d(TAG, "Evicting P2P " + mToEvictP2p); | |
| for (Uri uri : mToEvictP2p) { | |
| mCache.remove(uri); | |
| } | |
| mToEvictP2p.clear(); | |
| } | |
| } | |
| }, WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION); | |
| mWifiMonitor = new WifiMonitor(service, connected -> { | |
| if (!connected) { | |
| // Evict specified device capabilities when network is lost. | |
| if (DEBUG) Log.d(TAG, "Evicting Wi-Fi " + mToEvict); | |
| for (Uri uri : mToEvict) { | |
| mCache.remove(uri); | |
| } | |
| mToEvict.clear(); | |
| } | |
| }); | |
| } | |
| @Override | |
| public void close() { | |
| if (DEBUG) Log.d(TAG, "stop()"); | |
| mIsStopped = true; | |
| mWifiMonitor.close(); | |
| mP2pMonitor.close(); | |
| } | |
| /** Callback for receiving capabilities */ | |
| public interface OnLocalPrinterCapabilities { | |
| /** Called when capabilities are retrieved */ | |
| void onCapabilities(LocalPrinterCapabilities capabilities); | |
| } | |
| /** | |
| * Query capabilities and return full results to the listener. A full result includes | |
| * enough backend data and is suitable for printing. If full data is already available | |
| * it will be returned to the callback immediately. | |
| * | |
| * @param highPriority if true, perform this query before others | |
| * @param onLocalPrinterCapabilities listener to receive capabilities. Receives null | |
| * if the attempt fails | |
| */ | |
| public void request(DiscoveredPrinter printer, boolean highPriority, | |
| OnLocalPrinterCapabilities onLocalPrinterCapabilities) { | |
| if (DEBUG) Log.d(TAG, "request() printer=" + printer + " high=" + highPriority); | |
| LocalPrinterCapabilities capabilities = get(printer); | |
| if (capabilities != null && capabilities.nativeData != null) { | |
| onLocalPrinterCapabilities.onCapabilities(capabilities); | |
| return; | |
| } | |
| if (P2pUtils.isOnConnectedInterface(mService, printer)) { | |
| if (DEBUG) Log.d(TAG, "Adding to P2P evict list: " + printer); | |
| mToEvictP2p.add(printer.path); | |
| } else { | |
| if (DEBUG) Log.d(TAG, "Adding to WLAN evict list: " + printer); | |
| mToEvict.add(printer.path); | |
| } | |
| // Create a new request with timeout based on priority | |
| Request request = mRequests.computeIfAbsent(printer.path, uri -> | |
| new Request(printer, highPriority ? SECOND_PASS_TIMEOUT : FIRST_PASS_TIMEOUT)); | |
| if (highPriority) { | |
| request.mHighPriority = true; | |
| } | |
| request.mCallbacks.add(onLocalPrinterCapabilities); | |
| startNextRequest(); | |
| } | |
| /** | |
| * Returns capabilities for the specified printer, if known | |
| */ | |
| public LocalPrinterCapabilities get(DiscoveredPrinter printer) { | |
| LocalPrinterCapabilities capabilities = mCache.get(printer.path); | |
| // Populate certificate from store if possible | |
| if (capabilities != null) { | |
| capabilities.certificate = mService.getCertificateStore().get(capabilities.uuid); | |
| } | |
| return capabilities; | |
| } | |
| /** | |
| * Remove capabilities corresponding to a Printer URI | |
| * @return The removed capabilities, if any | |
| */ | |
| public LocalPrinterCapabilities remove(Uri printerUri) { | |
| return mCache.remove(printerUri); | |
| } | |
| /** | |
| * Cancel all outstanding attempts to get capabilities for this callback | |
| */ | |
| public void cancel(OnLocalPrinterCapabilities onLocalPrinterCapabilities) { | |
| List<Uri> toDrop = new ArrayList<>(); | |
| for (Map.Entry<Uri, Request> entry : mRequests.entrySet()) { | |
| Request request = entry.getValue(); | |
| request.mCallbacks.remove(onLocalPrinterCapabilities); | |
| if (request.mCallbacks.isEmpty()) { | |
| toDrop.add(entry.getKey()); | |
| request.cancel(); | |
| } | |
| } | |
| for (Uri request : toDrop) { | |
| mRequests.remove(request); | |
| } | |
| } | |
| /** Look for next query and launch it */ | |
| private void startNextRequest() { | |
| final Request request = getNextRequest(); | |
| if (request == null) { | |
| return; | |
| } | |
| request.start(); | |
| } | |
| /** Return the next request if it is appropriate to perform one */ | |
| private Request getNextRequest() { | |
| Request found = null; | |
| int total = 0; | |
| for (Request request : mRequests.values()) { | |
| if (request.mQuery != null) { | |
| total++; | |
| } else if (found == null || (!found.mHighPriority && request.mHighPriority) | |
| || (found.mHighPriority == request.mHighPriority | |
| && request.mTimeout < found.mTimeout)) { | |
| // First valid or higher priority request | |
| found = request; | |
| } | |
| } | |
| if (total >= mMaxConcurrent) { | |
| return null; | |
| } | |
| return found; | |
| } | |
| /** Holds an outstanding capabilities request */ | |
| public class Request implements Consumer<LocalPrinterCapabilities> { | |
| final DiscoveredPrinter mPrinter; | |
| final List<OnLocalPrinterCapabilities> mCallbacks = new ArrayList<>(); | |
| GetCapabilitiesTask mQuery; | |
| boolean mHighPriority = false; | |
| long mTimeout; | |
| Request(DiscoveredPrinter printer, long timeout) { | |
| mPrinter = printer; | |
| mTimeout = timeout; | |
| } | |
| private void start() { | |
| mQuery = mBackend.getCapabilities(mPrinter.path, mTimeout, mHighPriority, this); | |
| } | |
| private void cancel() { | |
| if (mQuery != null) { | |
| mQuery.forceCancel(); | |
| mQuery = null; | |
| } | |
| } | |
| @Override | |
| public void accept(LocalPrinterCapabilities capabilities) { | |
| DiscoveredPrinter printer = mPrinter; | |
| if (DEBUG) Log.d(TAG, "Capabilities for " + printer + " cap=" + capabilities); | |
| if (mIsStopped) { | |
| return; | |
| } | |
| mRequests.remove(printer.path); | |
| // Grab uuid from capabilities if possible | |
| Uri capUuid = null; | |
| if (capabilities != null) { | |
| if (!TextUtils.isEmpty(capabilities.uuid)) { | |
| capUuid = Uri.parse(capabilities.uuid); | |
| } | |
| if (printer.uuid != null && !printer.uuid.equals(capUuid)) { | |
| Log.w(TAG, "UUID mismatch for " + printer + "; rejecting capabilities"); | |
| capabilities = null; | |
| } | |
| } | |
| if (capabilities == null) { | |
| if (mTimeout == FIRST_PASS_TIMEOUT) { | |
| // Printer did not respond quickly, try again in the slow lane | |
| mTimeout = SECOND_PASS_TIMEOUT; | |
| mQuery = null; | |
| mRequests.put(printer.path, this); | |
| startNextRequest(); | |
| return; | |
| } else { | |
| mCache.remove(printer.getUri()); | |
| } | |
| } else { | |
| capabilities.certificate = mService.getCertificateStore().get(capabilities.uuid); | |
| mCache.put(printer.path, capabilities); | |
| } | |
| LocalPrinterCapabilities result = capabilities; | |
| for (OnLocalPrinterCapabilities callback : mCallbacks) { | |
| callback.onCapabilities(result); | |
| } | |
| startNextRequest(); | |
| } | |
| } | |
| } |