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.discovery;
19 
20 import android.net.Uri;
21 import android.text.TextUtils;
22 import android.util.Log;
23 
24 import com.android.bips.BuiltInPrintService;
25 import com.android.bips.ipp.CapabilitiesCache;
26 import com.android.bips.jni.LocalPrinterCapabilities;
27 import com.android.bips.util.WifiMonitor;
28 
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.LinkedHashSet;
32 import java.util.List;
33 import java.util.Set;
34 
35 /**
36  * Manage a list of printers manually added by the user.
37  */
38 public class ManualDiscovery extends SavedDiscovery {
39     private static final String TAG = ManualDiscovery.class.getSimpleName();
40     private static final boolean DEBUG = false;
41 
42     // Likely paths at which a print service may be found
43     private static final Uri[] IPP_URIS = {Uri.parse("ipp://host:631/ipp/print"),
44             Uri.parse("ipp://host:80/ipp/print"), Uri.parse("ipp://host:631/ipp/printer"),
45             Uri.parse("ipp://host:631/ipp"), Uri.parse("ipp://host:631/"),
46             Uri.parse("ipps://host:631/ipp/print"), Uri.parse("ipps://host:443/ipp/print"),
47             Uri.parse("ipps://host:10443/ipp/print")};
48 
49     private WifiMonitor mWifiMonitor;
50     private CapabilitiesCache mCapabilitiesCache;
51     private List<CapabilitiesFinder> mAddRequests = new ArrayList<>();
52 
ManualDiscovery(BuiltInPrintService printService)53     public ManualDiscovery(BuiltInPrintService printService) {
54         super(printService);
55         mCapabilitiesCache = getPrintService().getCapabilitiesCache();
56     }
57 
58     @Override
onStart()59     void onStart() {
60         if (DEBUG) Log.d(TAG, "onStart");
61 
62         // Upon any network change scan for all manually added printers
63         mWifiMonitor = new WifiMonitor(getPrintService(), isConnected -> {
64             if (isConnected) {
65                 for (DiscoveredPrinter printer : getSavedPrinters()) {
66                     mCapabilitiesCache.request(printer, false, capabilities -> {
67                         if (capabilities != null) {
68                             printerFound(printer);
69                         }
70                     });
71                 }
72             } else {
73                 allPrintersLost();
74             }
75         });
76     }
77 
78     @Override
onStop()79     void onStop() {
80         if (DEBUG) Log.d(TAG, "onStop");
81         mWifiMonitor.close();
82         allPrintersLost();
83     }
84 
85     /**
86      * Asynchronously attempt to add a new manual printer, calling back with success if
87      * printer capabilities were discovered.
88      *
89      * The supplied URI must include a hostname and may also include a scheme (either ipp:// or
90      * ipps://), a port (such as :443), and/or a path (like /ipp/print). If any parts are missing,
91      * typical known values are substituted and searched until success is found, or all are
92      * tried unsuccessfully.
93      *
94      * @param printerUri URI to search
95      */
addManualPrinter(Uri printerUri, PrinterAddCallback callback)96     public void addManualPrinter(Uri printerUri, PrinterAddCallback callback) {
97         if (DEBUG) Log.d(TAG, "addManualPrinter " + printerUri);
98 
99         int givenPort = printerUri.getPort();
100         String givenPath = printerUri.getPath();
101         String hostname = printerUri.getHost();
102         String givenScheme = printerUri.getScheme();
103 
104         // Use LinkedHashSet to eliminate duplicates but maintain order
105         Set<Uri> uris = new LinkedHashSet<>();
106         for (Uri uri : IPP_URIS) {
107             String scheme = uri.getScheme();
108             if (!TextUtils.isEmpty(givenScheme) && !scheme.equals(givenScheme)) {
109                 // If scheme was supplied and doesn't match this uri template, skip
110                 continue;
111             }
112             String authority = hostname + ":" + (givenPort == -1 ? uri.getPort() : givenPort);
113             String path = TextUtils.isEmpty(givenPath) ? uri.getPath() : givenPath;
114             Uri targetUri = uri.buildUpon().scheme(scheme).encodedAuthority(authority).path(path)
115                     .build();
116             uris.add(targetUri);
117         }
118 
119         mAddRequests.add(new CapabilitiesFinder(uris, callback));
120     }
121 
122     /**
123      * Cancel a prior {@link #addManualPrinter(Uri, PrinterAddCallback)} attempt having the same
124      * callback
125      */
cancelAddManualPrinter(PrinterAddCallback callback)126     public void cancelAddManualPrinter(PrinterAddCallback callback) {
127         for (CapabilitiesFinder finder : mAddRequests) {
128             if (finder.mFinalCallback == callback) {
129                 mAddRequests.remove(finder);
130                 finder.cancel();
131                 return;
132             }
133         }
134     }
135 
136     /** Used to convey response to {@link #addManualPrinter} */
137     public interface PrinterAddCallback {
138         /**
139          * The requested manual printer was found.
140          *
141          * @param printer   information about the discovered printer
142          * @param supported true if the printer is supported (and was therefore added), or false
143          *                  if the printer was found but is not supported (and was therefore not
144          *                  added)
145          */
onFound(DiscoveredPrinter printer, boolean supported)146         void onFound(DiscoveredPrinter printer, boolean supported);
147 
148         /**
149          * The requested manual printer was not found.
150          */
onNotFound()151         void onNotFound();
152     }
153 
154     /**
155      * Search common printer paths for a successful response
156      */
157     private class CapabilitiesFinder {
158         private final PrinterAddCallback mFinalCallback;
159         private final List<CapabilitiesCache.OnLocalPrinterCapabilities> mRequests =
160                 new ArrayList<>();
161 
162         /**
163          * Constructs a new finder
164          *
165          * @param uris     Locations to check for IPP endpoints
166          * @param callback Callback to issue when the first successful response arrives, or
167          *                 when all responses have failed.
168          */
CapabilitiesFinder(Collection<Uri> uris, PrinterAddCallback callback)169         CapabilitiesFinder(Collection<Uri> uris, PrinterAddCallback callback) {
170             mFinalCallback = callback;
171             for (Uri uri : uris) {
172                 CapabilitiesCache.OnLocalPrinterCapabilities capabilitiesCallback =
173                         new CapabilitiesCache.OnLocalPrinterCapabilities() {
174                             @Override
175                             public void onCapabilities(LocalPrinterCapabilities capabilities) {
176                                 mRequests.remove(this);
177                                 handleCapabilities(uri, capabilities);
178                             }
179                         };
180                 mRequests.add(capabilitiesCallback);
181 
182                 // Force a clean attempt from scratch
183                 mCapabilitiesCache.remove(uri);
184                 mCapabilitiesCache.request(new DiscoveredPrinter(null, "", uri, null),
185                         true, capabilitiesCallback);
186             }
187         }
188 
189         /** Capabilities have arrived (or not) for the printer at a given path */
handleCapabilities(Uri printerPath, LocalPrinterCapabilities capabilities)190         void handleCapabilities(Uri printerPath, LocalPrinterCapabilities capabilities) {
191             if (DEBUG) Log.d(TAG, "request " + printerPath + " cap=" + capabilities);
192 
193             if (capabilities == null) {
194                 if (mRequests.isEmpty()) {
195                     mAddRequests.remove(this);
196                     mFinalCallback.onNotFound();
197                 }
198                 return;
199             }
200 
201             // Success, so cancel all other requests
202             for (CapabilitiesCache.OnLocalPrinterCapabilities request : mRequests) {
203                 mCapabilitiesCache.cancel(request);
204             }
205             mRequests.clear();
206 
207             // Deliver a successful response
208             Uri uuid = TextUtils.isEmpty(capabilities.uuid) ? null : Uri.parse(capabilities.uuid);
209             String name = TextUtils.isEmpty(capabilities.name) ? printerPath.getHost()
210                     : capabilities.name;
211 
212             DiscoveredPrinter resolvedPrinter = new DiscoveredPrinter(uuid, name, printerPath,
213                     capabilities.location);
214 
215             // Only add supported printers
216             if (capabilities.isSupported) {
217                 if (addSavedPrinter(resolvedPrinter)) {
218                     printerFound(resolvedPrinter);
219                 }
220             }
221             mAddRequests.remove(this);
222             mFinalCallback.onFound(resolvedPrinter, capabilities.isSupported);
223         }
224 
225         /** Stop all in-progress capability requests that are in progress */
cancel()226         public void cancel() {
227             for (CapabilitiesCache.OnLocalPrinterCapabilities callback : mRequests) {
228                 mCapabilitiesCache.cancel(callback);
229             }
230             mRequests.clear();
231         }
232     }
233 }
234