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.JsonReader; 23 import android.util.JsonWriter; 24 import android.util.Log; 25 26 import com.android.bips.BuiltInPrintService; 27 import com.android.bips.ipp.CapabilitiesCache; 28 import com.android.bips.jni.LocalPrinterCapabilities; 29 30 import java.io.BufferedReader; 31 import java.io.BufferedWriter; 32 import java.io.File; 33 import java.io.FileReader; 34 import java.io.FileWriter; 35 import java.io.IOException; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.LinkedList; 39 import java.util.List; 40 41 /** 42 * Manage a list of printers manually added by the user. 43 */ 44 public class ManualDiscovery extends Discovery implements AutoCloseable { 45 private static final String TAG = ManualDiscovery.class.getSimpleName(); 46 private static final boolean DEBUG = false; 47 48 private static final String CACHE_FILE = TAG + ".json"; 49 50 // Likely paths at which a print service may be found 51 private static final String[] IPP_PATHS = {"ipp/printer", "ipp/print", "ipp", ""}; 52 53 private static final int DEFAULT_IPP_PORT = 631; 54 private static final String DEFAULT_IPP_SCHEME = "ipp"; 55 56 private final List<DiscoveredPrinter> mManualPrinters = new ArrayList<>(); 57 ManualDiscovery(BuiltInPrintService printService)58 public ManualDiscovery(BuiltInPrintService printService) { 59 super(printService); 60 load(); 61 } 62 63 @Override onStart()64 void onStart() { 65 if (DEBUG) Log.d(TAG, "onStart"); 66 for (DiscoveredPrinter printer : mManualPrinters) { 67 if (DEBUG) Log.d(TAG, "reporting " + printer); 68 getPrintService().getCapabilitiesCache().evictOnNetworkChange(printer.getUri()); 69 printerFound(printer); 70 } 71 } 72 73 @Override onStop()74 void onStop() { 75 if (DEBUG) Log.d(TAG, "onStop"); 76 } 77 78 @Override close()79 public void close() { 80 if (DEBUG) Log.d(TAG, "close"); 81 save(); 82 } 83 84 /** 85 * Asynchronously attempt to add a new manual printer, calling back with success 86 */ addManualPrinter(String hostname, PrinterAddCallback callback)87 public void addManualPrinter(String hostname, PrinterAddCallback callback) { 88 if (DEBUG) Log.d(TAG, "addManualPrinter " + hostname); 89 90 // Repair supplied hostname as much as possible 91 Uri base = Uri.parse(DEFAULT_IPP_SCHEME + "://" + hostname + ":" + DEFAULT_IPP_PORT); 92 93 new CapabilitiesFinder(base, callback).startNext(); 94 } 95 addManualPrinter(DiscoveredPrinter printer)96 private void addManualPrinter(DiscoveredPrinter printer) { 97 // Remove any prior printer with the same uri or path 98 for (DiscoveredPrinter other : new ArrayList<>(mManualPrinters)) { 99 if (other.getUri().equals(printer.getUri())) { 100 printerLost(other.getUri()); 101 mManualPrinters.remove(other); 102 } 103 } 104 105 // Add new printer at top 106 mManualPrinters.add(0, printer); 107 108 // Notify if necessary 109 if (isStarted()) printerFound(printer); 110 } 111 112 /** 113 * Remove an existing manual printer 114 */ removeManualPrinter(DiscoveredPrinter toRemove)115 public void removeManualPrinter(DiscoveredPrinter toRemove) { 116 for (DiscoveredPrinter printer : mManualPrinters) { 117 if (printer.path.equals(toRemove.path)) { 118 mManualPrinters.remove(printer); 119 if (isStarted()) { 120 printerLost(printer.getUri()); 121 } 122 break; 123 } 124 } 125 } 126 127 /** 128 * Persist the current set of manual printers to storage 129 */ save()130 private void save() { 131 File cachedPrintersFile = new File(getPrintService().getCacheDir(), CACHE_FILE); 132 if (cachedPrintersFile.exists()) { 133 cachedPrintersFile.delete(); 134 } 135 136 try (JsonWriter writer = new JsonWriter(new BufferedWriter( 137 new FileWriter(cachedPrintersFile)))) { 138 writer.beginObject(); 139 writer.name("manualPrinters"); 140 writer.beginArray(); 141 for (DiscoveredPrinter printer : mManualPrinters) { 142 if (DEBUG) Log.d(TAG, "Writing " + printer); 143 printer.write(writer); 144 } 145 writer.endArray(); 146 writer.endObject(); 147 } catch (NullPointerException | IOException e) { 148 Log.w(TAG, "Error while storing", e); 149 } 150 } 151 152 /** 153 * Load the current set of manual printers from storage 154 */ load()155 private void load() { 156 File cachedPrintersFile = new File(getPrintService().getCacheDir(), CACHE_FILE); 157 if (!cachedPrintersFile.exists()) return; 158 159 try (JsonReader reader = new JsonReader(new BufferedReader( 160 new FileReader(cachedPrintersFile)))) { 161 reader.beginObject(); 162 while (reader.hasNext()) { 163 String itemName = reader.nextName(); 164 if (itemName.equals("manualPrinters")) { 165 reader.beginArray(); 166 while (reader.hasNext()) { 167 addManualPrinter(new DiscoveredPrinter(reader)); 168 } 169 reader.endArray(); 170 } 171 } 172 reader.endObject(); 173 } catch (IllegalStateException | IOException ignored) { 174 Log.w(TAG, "Error while restoring", ignored); 175 } 176 if (DEBUG) Log.d(TAG, "After load we have " + mManualPrinters.size() + " manual printers"); 177 } 178 179 /** Used to convey response to {@link #addManualPrinter} */ 180 public interface PrinterAddCallback { 181 /** 182 * The requested manual printer was found. 183 * 184 * @param printer information about the discovered printer 185 * @param supported true if the printer is supported (and was therefore added), or false 186 * if the printer was found but is not supported (and was therefore not 187 * added) 188 */ onFound(DiscoveredPrinter printer, boolean supported)189 void onFound(DiscoveredPrinter printer, boolean supported); 190 191 /** 192 * The requested manual printer was not found. 193 */ onNotFound()194 void onNotFound(); 195 } 196 197 /** 198 * Search common printer paths for a successful response 199 */ 200 private class CapabilitiesFinder implements CapabilitiesCache.OnLocalPrinterCapabilities { 201 private final LinkedList<String> mPaths = new LinkedList<>(); 202 private final PrinterAddCallback mFinalCallback; 203 private final Uri mBase; 204 205 /** 206 * Constructs a new callback handler 207 * 208 * @param base Base URI for print service to find 209 * @param callback Callback to issue when the first successful response arrives, or 210 * when all responses have failed. 211 */ CapabilitiesFinder(Uri base, PrinterAddCallback callback)212 CapabilitiesFinder(Uri base, PrinterAddCallback callback) { 213 mPaths.addAll(Arrays.asList(IPP_PATHS)); 214 mFinalCallback = callback; 215 mBase = base; 216 } 217 218 /** Move on to the next path or report failure if none remain */ startNext()219 void startNext() { 220 if (mPaths.isEmpty()) { 221 mFinalCallback.onNotFound(); 222 } else { 223 Uri uriToTry = mBase.buildUpon().encodedPath(mPaths.pop()).build(); 224 DiscoveredPrinter printer = new DiscoveredPrinter(null, "unknown", uriToTry, null); 225 getPrintService().getCapabilitiesCache().request(printer, false, this); 226 } 227 } 228 229 @Override onCapabilities(LocalPrinterCapabilities capabilities)230 public void onCapabilities(LocalPrinterCapabilities capabilities) { 231 if (DEBUG) Log.d(TAG, "onCapabilities: " + capabilities); 232 233 if (capabilities == null) { 234 startNext(); 235 return; 236 } 237 238 // Deliver a successful response 239 Uri path = Uri.parse(capabilities.path); 240 if (path.getPort() == -1) { 241 // Fix missing port 242 path = path.buildUpon().encodedAuthority(path.getAuthority() + ":" + 243 DEFAULT_IPP_PORT).build(); 244 } 245 Uri uuid = TextUtils.isEmpty(capabilities.uuid) ? null : Uri.parse(capabilities.uuid); 246 String name = TextUtils.isEmpty(capabilities.name) ? path.getHost() : capabilities.name; 247 248 DiscoveredPrinter printer = new DiscoveredPrinter(uuid, name, path, 249 capabilities.location); 250 251 // Only add supported printers 252 if (capabilities.isSupported) { 253 addManualPrinter(printer); 254 } 255 256 mFinalCallback.onFound(printer, capabilities.isSupported); 257 } 258 } 259 }