/* * 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 mCache = new LruCache<>(CACHE_SIZE); // Outstanding requests based on printer path private final Map mRequests = new HashMap<>(); private final Set mToEvict = new HashSet<>(); private final Set 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 toDrop = new ArrayList<>(); for (Map.Entry 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 { final DiscoveredPrinter mPrinter; final List 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(); } } }