|  | @@ -54,6 +54,8 @@ import java.util.Collections;
 | 
	
		
			
				|  |  |  import java.util.Enumeration;
 | 
	
		
			
				|  |  |  import java.util.HashMap;
 | 
	
		
			
				|  |  |  import java.util.List;
 | 
	
		
			
				|  |  | +import java.util.Locale;
 | 
	
		
			
				|  |  | +import java.util.regex.Pattern;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  /**
 | 
	
		
			
				|  |  |   * Allows the user to configue a master {@link URI} then it returns that
 | 
	
	
		
			
				|  | @@ -80,6 +82,24 @@ public class MasterChooser extends Activity {
 | 
	
		
			
				|  |  |    private static final String BAR_CODE_SCANNER_PACKAGE_NAME =
 | 
	
		
			
				|  |  |        "com.google.zxing.client.android.SCAN";
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Lookup text for catching a ConnectionException when attempting to
 | 
	
		
			
				|  |  | +   * connect to a master.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private static final String CONNECTION_EXCEPTION_TEXT = "ECONNREFUSED";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Lookup text for catching a UnknownHostException when attemping to
 | 
	
		
			
				|  |  | +   * connect to a master.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private static final String UNKNOW_HOST_TEXT = "UnknownHost";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Default port number for master URI. Apended if the URI does not
 | 
	
		
			
				|  |  | +   * contain a port number.
 | 
	
		
			
				|  |  | +   */
 | 
	
		
			
				|  |  | +  private static final int DEFAULT_PORT = 11311;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    private String selectedInterface;
 | 
	
		
			
				|  |  |    private EditText uriText;
 | 
	
		
			
				|  |  |    private Button connectButton;
 | 
	
	
		
			
				|  | @@ -111,16 +131,21 @@ public class MasterChooser extends Activity {
 | 
	
		
			
				|  |  |    protected void onCreate(Bundle savedInstanceState) {
 | 
	
		
			
				|  |  |      super.onCreate(savedInstanceState);
 | 
	
		
			
				|  |  |      setContentView(R.layout.master_chooser);
 | 
	
		
			
				|  |  | +    final Pattern uriPattern = RosURIPattern.URI;
 | 
	
		
			
				|  |  |      uriText = (EditText) findViewById(R.id.master_chooser_uri);
 | 
	
		
			
				|  |  |      connectButton = (Button) findViewById(R.id.master_chooser_ok);
 | 
	
		
			
				|  |  |      uriText.addTextChangedListener(new TextWatcher() {
 | 
	
		
			
				|  |  |        @Override
 | 
	
		
			
				|  |  |        public void onTextChanged(CharSequence s, int start, int before, int count) {
 | 
	
		
			
				|  |  | -        if (s.length() > 0) {
 | 
	
		
			
				|  |  | -          connectButton.setEnabled(true);
 | 
	
		
			
				|  |  | -        } else {
 | 
	
		
			
				|  |  | +        final String uri = s.toString();
 | 
	
		
			
				|  |  | +        if(!uriPattern.matcher(uri).matches()) {
 | 
	
		
			
				|  |  | +          uriText.setError("Please enter valid URI");
 | 
	
		
			
				|  |  |            connectButton.setEnabled(false);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | +        else {
 | 
	
		
			
				|  |  | +          uriText.setError(null);
 | 
	
		
			
				|  |  | +          connectButton.setEnabled(true);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |        @Override
 | 
	
	
		
			
				|  | @@ -181,10 +206,24 @@ public class MasterChooser extends Activity {
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    public void okButtonClicked(View unused) {
 | 
	
		
			
				|  |  | +    String tmpURI = uriText.getText().toString();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // Check to see if the URI has a port.
 | 
	
		
			
				|  |  | +    final Pattern portPattern = RosURIPattern.PORT;
 | 
	
		
			
				|  |  | +    if(!portPattern.matcher(tmpURI).find()) {
 | 
	
		
			
				|  |  | +      // Append the default port to the URI and update the TextView.
 | 
	
		
			
				|  |  | +      tmpURI = String.format(Locale.getDefault(),"%s:%d/",tmpURI,DEFAULT_PORT);
 | 
	
		
			
				|  |  | +      uriText.setText(tmpURI);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // Set the URI for connection.
 | 
	
		
			
				|  |  | +    final String uri = tmpURI;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      // Prevent further edits while we verify the URI.
 | 
	
		
			
				|  |  | +    // Note: This was placed after the URI port check due to odd behavior
 | 
	
		
			
				|  |  | +    // with setting the connectButton to disabled.
 | 
	
		
			
				|  |  |      uriText.setEnabled(false);
 | 
	
		
			
				|  |  |      connectButton.setEnabled(false);
 | 
	
		
			
				|  |  | -    final String uri = uriText.getText().toString();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      // Make sure the URI can be parsed correctly and that the master is
 | 
	
		
			
				|  |  |      // reachable.
 | 
	
	
		
			
				|  | @@ -204,6 +243,16 @@ public class MasterChooser extends Activity {
 | 
	
		
			
				|  |  |            toast("Master unreachable!");
 | 
	
		
			
				|  |  |            return false;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | +        catch (Exception e) {
 | 
	
		
			
				|  |  | +          String exceptionMessage = e.getMessage();
 | 
	
		
			
				|  |  | +          if(exceptionMessage.contains(CONNECTION_EXCEPTION_TEXT))
 | 
	
		
			
				|  |  | +            toast("Unable to communicate with master!");
 | 
	
		
			
				|  |  | +          else if(exceptionMessage.contains(UNKNOW_HOST_TEXT))
 | 
	
		
			
				|  |  | +            toast("Unable to resolve URI hostname!");
 | 
	
		
			
				|  |  | +          else
 | 
	
		
			
				|  |  | +            toast("Communication error!");
 | 
	
		
			
				|  |  | +          return false;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |        @Override
 | 
	
	
		
			
				|  | @@ -297,4 +346,87 @@ public class MasterChooser extends Activity {
 | 
	
		
			
				|  |  |          getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
 | 
	
		
			
				|  |  |      return (list.size() > 0);
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /**
 | 
	
		
			
				|  |  | +   * Regular expressions used with ROS URIs.
 | 
	
		
			
				|  |  | +   *
 | 
	
		
			
				|  |  | +   * The majority of the expressions and variables were copied from
 | 
	
		
			
				|  |  | +   * {@link android.util.Patterns}. The {@link android.util.Patterns} class could not be
 | 
	
		
			
				|  |  | +   * utilized because the PROTOCOL regex included other web protocols besides http. The
 | 
	
		
			
				|  |  | +   * http protocol is required by ROS.
 | 
	
		
			
				|  |  | +  */
 | 
	
		
			
				|  |  | +  private static class RosURIPattern
 | 
	
		
			
				|  |  | +  {
 | 
	
		
			
				|  |  | +    /* A word boundary or end of input.  This is to stop foo.sure from matching as foo.su */
 | 
	
		
			
				|  |  | +    private static final String WORD_BOUNDARY = "(?:\\b|$|^)";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * Valid UCS characters defined in RFC 3987. Excludes space characters.
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    private static final String UCS_CHAR = "[" +
 | 
	
		
			
				|  |  | +            "\u00A0-\uD7FF" +
 | 
	
		
			
				|  |  | +            "\uF900-\uFDCF" +
 | 
	
		
			
				|  |  | +            "\uFDF0-\uFFEF" +
 | 
	
		
			
				|  |  | +            "\uD800\uDC00-\uD83F\uDFFD" +
 | 
	
		
			
				|  |  | +            "\uD840\uDC00-\uD87F\uDFFD" +
 | 
	
		
			
				|  |  | +            "\uD880\uDC00-\uD8BF\uDFFD" +
 | 
	
		
			
				|  |  | +            "\uD8C0\uDC00-\uD8FF\uDFFD" +
 | 
	
		
			
				|  |  | +            "\uD900\uDC00-\uD93F\uDFFD" +
 | 
	
		
			
				|  |  | +            "\uD940\uDC00-\uD97F\uDFFD" +
 | 
	
		
			
				|  |  | +            "\uD980\uDC00-\uD9BF\uDFFD" +
 | 
	
		
			
				|  |  | +            "\uD9C0\uDC00-\uD9FF\uDFFD" +
 | 
	
		
			
				|  |  | +            "\uDA00\uDC00-\uDA3F\uDFFD" +
 | 
	
		
			
				|  |  | +            "\uDA40\uDC00-\uDA7F\uDFFD" +
 | 
	
		
			
				|  |  | +            "\uDA80\uDC00-\uDABF\uDFFD" +
 | 
	
		
			
				|  |  | +            "\uDAC0\uDC00-\uDAFF\uDFFD" +
 | 
	
		
			
				|  |  | +            "\uDB00\uDC00-\uDB3F\uDFFD" +
 | 
	
		
			
				|  |  | +            "\uDB44\uDC00-\uDB7F\uDFFD" +
 | 
	
		
			
				|  |  | +            "&&[^\u00A0[\u2000-\u200A]\u2028\u2029\u202F\u3000]]";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * Valid characters for IRI label defined in RFC 3987.
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    private static final String LABEL_CHAR = "a-zA-Z0-9" + UCS_CHAR;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets.
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    private static final String IRI_LABEL =
 | 
	
		
			
				|  |  | +            "[" + LABEL_CHAR + "](?:[" + LABEL_CHAR + "\\-]{0,61}[" + LABEL_CHAR + "]){0,1}";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private static final Pattern IP_ADDRESS
 | 
	
		
			
				|  |  | +            = Pattern.compile(
 | 
	
		
			
				|  |  | +            "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]"
 | 
	
		
			
				|  |  | +                    + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
 | 
	
		
			
				|  |  | +                    + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
 | 
	
		
			
				|  |  | +                    + "|[1-9][0-9]|[0-9]))");
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * Regular expression that matches domain names without a TLD
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    private static final String RELAXED_DOMAIN_NAME =
 | 
	
		
			
				|  |  | +            "(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" +"?)+" +
 | 
	
		
			
				|  |  | +                    "|" + IP_ADDRESS + ")";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private static final String HTTP_PROTOCOL = "(?i:http):\\/\\/";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private static final String PORT_NUMBER = "\\:\\d{1,5}\\/?";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     *  Regular expression pattern to match valid rosmaster URIs.
 | 
	
		
			
				|  |  | +     *  This assumes the port number and trailing "/" will be auto
 | 
	
		
			
				|  |  | +     *  populated (default port: 11311) if left out.
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    public static final Pattern URI = Pattern.compile("("
 | 
	
		
			
				|  |  | +            + WORD_BOUNDARY
 | 
	
		
			
				|  |  | +            + "(?:"
 | 
	
		
			
				|  |  | +            + "(?:" + HTTP_PROTOCOL + ")"
 | 
	
		
			
				|  |  | +            + "(?:" + RELAXED_DOMAIN_NAME + ")"
 | 
	
		
			
				|  |  | +            + "(?:" + PORT_NUMBER + ")?"
 | 
	
		
			
				|  |  | +            + ")"
 | 
	
		
			
				|  |  | +            + WORD_BOUNDARY
 | 
	
		
			
				|  |  | +            + ")");
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public static final Pattern PORT = Pattern.compile(PORT_NUMBER);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  |  }
 |