Merge "Support manual-add of IPPS and other printers"
diff --git a/src/com/android/bips/discovery/ManualDiscovery.java b/src/com/android/bips/discovery/ManualDiscovery.java
index 083844c..4b8f9a1 100644
--- a/src/com/android/bips/discovery/ManualDiscovery.java
+++ b/src/com/android/bips/discovery/ManualDiscovery.java
@@ -27,7 +27,10 @@
 import com.android.bips.util.WifiMonitor;
 
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Manage a list of printers manually added by the user.
@@ -37,12 +40,15 @@
     private static final boolean DEBUG = false;
 
     // Likely paths at which a print service may be found
-    private static final Uri[] IPP_URIS = {Uri.parse("ipp://path:631/ipp/print"),
-            Uri.parse("ipp://path:80/ipp/print"), Uri.parse("ipp://path:631/ipp/printer"),
-            Uri.parse("ipp://path:631/ipp"), Uri.parse("ipp://path:631/")};
+    private static final Uri[] IPP_URIS = {Uri.parse("ipp://host:631/ipp/print"),
+            Uri.parse("ipp://host:80/ipp/print"), Uri.parse("ipp://host:631/ipp/printer"),
+            Uri.parse("ipp://host:631/ipp"), Uri.parse("ipp://host:631/"),
+            Uri.parse("ipps://host:631/ipp/print"), Uri.parse("ipps://host:443/ipp/print"),
+            Uri.parse("ipps://host:10443/ipp/print")};
 
     private WifiMonitor mWifiMonitor;
     private CapabilitiesCache mCapabilitiesCache;
+    private List<CapabilitiesFinder> mAddRequests = new ArrayList<>();
 
     public ManualDiscovery(BuiltInPrintService printService) {
         super(printService);
@@ -77,11 +83,54 @@
     }
 
     /**
-     * Asynchronously attempt to add a new manual printer, calling back with success
+     * Asynchronously attempt to add a new manual printer, calling back with success if
+     * printer capabilities were discovered.
+     *
+     * The supplied URI must include a hostname and may also include a scheme (either ipp:// or
+     * ipps://), a port (such as :443), and/or a path (like /ipp/print). If any parts are missing,
+     * typical known values are substituted and searched until success is found, or all are
+     * tried unsuccessfully.
+     *
+     * @param printerUri URI to search
      */
-    public void addManualPrinter(String hostname, PrinterAddCallback callback) {
-        if (DEBUG) Log.d(TAG, "addManualPrinter " + hostname);
-        new CapabilitiesFinder(hostname, callback);
+    public void addManualPrinter(Uri printerUri, PrinterAddCallback callback) {
+        if (DEBUG) Log.d(TAG, "addManualPrinter " + printerUri);
+
+        int givenPort = printerUri.getPort();
+        String givenPath = printerUri.getPath();
+        String hostname = printerUri.getHost();
+        String givenScheme = printerUri.getScheme();
+
+        // Use LinkedHashSet to eliminate duplicates but maintain order
+        Set<Uri> uris = new LinkedHashSet<>();
+        for (Uri uri : IPP_URIS) {
+            String scheme = uri.getScheme();
+            if (!TextUtils.isEmpty(givenScheme) && !scheme.equals(givenScheme)) {
+                // If scheme was supplied and doesn't match this uri template, skip
+                continue;
+            }
+            String authority = hostname + ":" + (givenPort == -1 ? uri.getPort() : givenPort);
+            String path = TextUtils.isEmpty(givenPath) ? uri.getPath() : givenPath;
+            Uri targetUri = uri.buildUpon().scheme(scheme).encodedAuthority(authority).path(path)
+                    .build();
+            uris.add(targetUri);
+        }
+
+        mAddRequests.add(new CapabilitiesFinder(uris, callback));
+    }
+
+    /**
+     * Cancel a prior {@link #addManualPrinter(Uri, PrinterAddCallback)} attempt having the same
+     * callback
+     */
+    public void cancelAddManualPrinter(PrinterAddCallback callback) {
+        for (CapabilitiesFinder finder : mAddRequests) {
+            if (finder.mFinalCallback == callback) {
+                mAddRequests.remove(finder);
+                finder.cancel();
+                return;
+            }
+        }
     }
 
     /** Used to convey response to {@link #addManualPrinter} */
@@ -113,27 +162,26 @@
         /**
          * Constructs a new finder
          *
-         * @param hostname Hostname to crawl for IPP endpoints
+         * @param uris     Locations to check for IPP endpoints
          * @param callback Callback to issue when the first successful response arrives, or
          *                 when all responses have failed.
          */
-        CapabilitiesFinder(String hostname, PrinterAddCallback callback) {
+        CapabilitiesFinder(Collection<Uri> uris, PrinterAddCallback callback) {
             mFinalCallback = callback;
-
-            for (Uri uri : IPP_URIS) {
-                Uri printerPath = uri.buildUpon().encodedAuthority(hostname + ":" + uri.getPort())
-                        .build();
+            for (Uri uri : uris) {
                 CapabilitiesCache.OnLocalPrinterCapabilities capabilitiesCallback =
                         new CapabilitiesCache.OnLocalPrinterCapabilities() {
                             @Override
                             public void onCapabilities(LocalPrinterCapabilities capabilities) {
                                 mRequests.remove(this);
-                                handleCapabilities(printerPath, capabilities);
+                                handleCapabilities(uri, capabilities);
                             }
                         };
                 mRequests.add(capabilitiesCallback);
 
-                mCapabilitiesCache.request(new DiscoveredPrinter(null, "", printerPath, null),
+                // Force a clean attempt from scratch
+                mCapabilitiesCache.remove(uri);
+                mCapabilitiesCache.request(new DiscoveredPrinter(null, "", uri, null),
                         true, capabilitiesCallback);
             }
         }
@@ -144,6 +192,7 @@
 
             if (capabilities == null) {
                 if (mRequests.isEmpty()) {
+                    mAddRequests.remove(this);
                     mFinalCallback.onNotFound();
                 }
                 return;
@@ -169,8 +218,16 @@
                     printerFound(resolvedPrinter);
                 }
             }
-
+            mAddRequests.remove(this);
             mFinalCallback.onFound(resolvedPrinter, capabilities.isSupported);
         }
+
+        /** Stop all in-progress capability requests that are in progress */
+        public void cancel() {
+            for (CapabilitiesCache.OnLocalPrinterCapabilities callback : mRequests) {
+                mCapabilitiesCache.cancel(callback);
+            }
+            mRequests.clear();
+        }
     }
 }
diff --git a/src/com/android/bips/ipp/Backend.java b/src/com/android/bips/ipp/Backend.java
index b3eed0f..94d6a11 100644
--- a/src/com/android/bips/ipp/Backend.java
+++ b/src/com/android/bips/ipp/Backend.java
@@ -89,16 +89,18 @@
     }
 
     /** Asynchronously get printer capabilities, returning results or null to a callback */
-    public AsyncTask<?, ?, ?> getCapabilities(Uri uri, long timeout, boolean highPriority,
+    public GetCapabilitiesTask getCapabilities(Uri uri, long timeout, boolean highPriority,
             final Consumer<LocalPrinterCapabilities> capabilitiesConsumer) {
         if (DEBUG) Log.d(TAG, "getCapabilities()");
 
-        return new GetCapabilitiesTask(this, uri, timeout, highPriority) {
+        GetCapabilitiesTask task = new GetCapabilitiesTask(this, uri, timeout, highPriority) {
             @Override
             protected void onPostExecute(LocalPrinterCapabilities result) {
                 capabilitiesConsumer.accept(result);
             }
-        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+        };
+        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+        return task;
     }
 
     /**
diff --git a/src/com/android/bips/ipp/CapabilitiesCache.java b/src/com/android/bips/ipp/CapabilitiesCache.java
index 6f51dee..dd4d258 100644
--- a/src/com/android/bips/ipp/CapabilitiesCache.java
+++ b/src/com/android/bips/ipp/CapabilitiesCache.java
@@ -230,7 +230,7 @@
     public class Request implements Consumer<LocalPrinterCapabilities> {
         final DiscoveredPrinter mPrinter;
         final List<OnLocalPrinterCapabilities> mCallbacks = new ArrayList<>();
-        AsyncTask<?, ?, ?> mQuery;
+        GetCapabilitiesTask mQuery;
         boolean mHighPriority = false;
         long mTimeout;
 
@@ -245,7 +245,7 @@
 
         private void cancel() {
             if (mQuery != null) {
-                mQuery.cancel(true);
+                mQuery.forceCancel();
                 mQuery = null;
             }
         }
diff --git a/src/com/android/bips/ipp/GetCapabilitiesTask.java b/src/com/android/bips/ipp/GetCapabilitiesTask.java
index 762390e..987e917 100644
--- a/src/com/android/bips/ipp/GetCapabilitiesTask.java
+++ b/src/com/android/bips/ipp/GetCapabilitiesTask.java
@@ -32,7 +32,7 @@
 import java.net.UnknownHostException;
 
 /** A background task that queries a specific URI for its complete capabilities */
-class GetCapabilitiesTask extends AsyncTask<Void, Void, LocalPrinterCapabilities> {
+public class GetCapabilitiesTask extends AsyncTask<Void, Void, LocalPrinterCapabilities> {
     private static final String TAG = GetCapabilitiesTask.class.getSimpleName();
     private static final boolean DEBUG = false;
 
@@ -43,6 +43,7 @@
     private final Uri mUri;
     private final long mTimeout;
     private final boolean mPriority;
+    private volatile Socket mSocket;
 
     GetCapabilitiesTask(Backend backend, Uri uri, long timeout, boolean priority) {
         mUri = uri;
@@ -53,11 +54,27 @@
 
     private boolean isDeviceOnline(Uri uri) {
         try (Socket socket = new Socket()) {
+            mSocket = socket;
             InetSocketAddress a = new InetSocketAddress(uri.getHost(), uri.getPort());
             socket.connect(a, (int) mTimeout);
             return true;
         } catch (IOException e) {
             return false;
+        } finally {
+            mSocket = null;
+        }
+    }
+
+    /** Forcibly cancel this task, including stopping any socket that was opened */
+    public void forceCancel() {
+        cancel(true);
+        Socket socket = mSocket;
+        if (socket != null) {
+            try {
+                socket.close();
+            } catch (IOException e) {
+                // Ignored
+            }
         }
     }
 
diff --git a/src/com/android/bips/ui/AddManualPrinterDialog.java b/src/com/android/bips/ui/AddManualPrinterDialog.java
index 5699b15..a46f5fd 100644
--- a/src/com/android/bips/ui/AddManualPrinterDialog.java
+++ b/src/com/android/bips/ui/AddManualPrinterDialog.java
@@ -21,6 +21,7 @@
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.Context;
+import android.net.Uri;
 import android.os.Bundle;
 import android.text.Editable;
 import android.text.TextWatcher;
@@ -46,18 +47,17 @@
  * Allows the user to enter printer address manually
  */
 class AddManualPrinterDialog extends AlertDialog implements TextWatcher,
-        TextView.OnEditorActionListener, View.OnKeyListener {
+        TextView.OnEditorActionListener, View.OnKeyListener, ManualDiscovery.PrinterAddCallback {
     private static final String TAG = AddManualPrinterDialog.class.getSimpleName();
     private static final boolean DEBUG = false;
 
     /**
-     * A regex that matches IP addresses and domain names like "192.168.1.101" and
-     * "printer1.company.com"
+     * A regex matching any printer URI with optional protocol, port and path.
      */
-    private static final String NAME_IP_REGEX =
-            "[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*(\\.[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*)*";
-    private static final String HOSTNAME_REGEXP = "^" + NAME_IP_REGEX + "$";
-    private static final Pattern HOSTNAME_PATTERN = Pattern.compile(HOSTNAME_REGEXP);
+    private static final String URI_REGEX =
+            "(ipp[s]?://)?[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*(\\.[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*)*(:[0-9]+)?(/.*)?";
+    private static final String FULL_URI_REGEX = "^" + URI_REGEX + "$";
+    private static final Pattern FULL_URI_PATTERN = Pattern.compile(FULL_URI_REGEX);
 
     private final ManualDiscovery mDiscovery;
     private final Activity mActivity;
@@ -85,8 +85,8 @@
 
         super.onCreate(savedInstanceState);
         mAddButton = getButton(AlertDialog.BUTTON_POSITIVE);
-        mHostnameView = (TextView) findViewById(R.id.hostname);
-        mProgressBar = (ProgressBar) findViewById(R.id.progress);
+        mHostnameView = findViewById(R.id.hostname);
+        mProgressBar = findViewById(R.id.progress);
 
         mAddButton.setOnClickListener(view1 -> addPrinter());
 
@@ -101,6 +101,13 @@
         updateButtonState();
     }
 
+    @Override
+    protected void onStop() {
+        if (DEBUG) Log.d(TAG, "onStop()");
+        super.onStop();
+        mDiscovery.cancelAddManualPrinter(this);
+    }
+
     private void openKeyboard(TextView view) {
         Window window = getWindow();
         if (window != null) {
@@ -115,9 +122,8 @@
 
     private void updateButtonState() {
         String hostname = mHostnameView.getText().toString();
-        Matcher hostMatch = HOSTNAME_PATTERN.matcher(hostname);
-
-        mAddButton.setEnabled(hostMatch.matches());
+        Matcher uriMatcher = FULL_URI_PATTERN.matcher(hostname);
+        mAddButton.setEnabled(uriMatcher.matches());
     }
 
     /** Attempt to add the printer based on current data */
@@ -128,24 +134,31 @@
         mProgressBar.setVisibility(View.VISIBLE);
 
         // Begin an attempt to add the printer
-        mDiscovery.addManualPrinter(mHostnameView.getText().toString(),
-                new ManualDiscovery.PrinterAddCallback() {
-                    @Override
-                    public void onFound(DiscoveredPrinter printer, boolean supported) {
-                        if (supported) {
-                            // Success case
-                            dismiss();
-                            mActivity.finish();
-                        } else {
-                            error(getContext().getString(R.string.printer_not_supported));
-                        }
-                    }
+        String uriString = mHostnameView.getText().toString();
+        Uri printerUri;
+        if (uriString.contains("://")) {
+            printerUri = Uri.parse(uriString);
+        } else {
+            // create a schemeless URI
+            printerUri = Uri.parse("://" + uriString);
+        }
+        mDiscovery.addManualPrinter(printerUri, this);
+    }
 
-                    @Override
-                    public void onNotFound() {
-                        error(getContext().getString(R.string.no_printer_found));
-                    }
-                });
+    @Override
+    public void onFound(DiscoveredPrinter printer, boolean supported) {
+        if (supported) {
+            // Success case
+            dismiss();
+            mActivity.finish();
+        } else {
+            error(getContext().getString(R.string.printer_not_supported));
+        }
+    }
+
+    @Override
+    public void onNotFound() {
+        error(getContext().getString(R.string.no_printer_found));
     }
 
     /** Inform user of error and allow them to correct it */
diff --git a/src/com/android/bips/ui/AddPrintersFragment.java b/src/com/android/bips/ui/AddPrintersFragment.java
index dc7b73e..7b51447 100644
--- a/src/com/android/bips/ui/AddPrintersFragment.java
+++ b/src/com/android/bips/ui/AddPrintersFragment.java
@@ -143,7 +143,7 @@
         if (P2pUtils.isP2p(printer)) {
             message = mPrintService.getString(R.string.connects_via_wifi_direct);
         } else {
-            message = mPrintService.getString(R.string.connects_via_network, printer.getHost());
+            message = mPrintService.getString(R.string.connects_via_network, printer.path);
         }
         new AlertDialog.Builder(getContext())
                 .setTitle(printer.name)