Global Proxy changes to proxy class.

Change-Id: Ib2da33670b1da33c0b35b60f4fcbd0bc084e616a
diff --git a/core/java/android/net/Proxy.java b/core/java/android/net/Proxy.java
index 22c30a5..188a4aa 100644
--- a/core/java/android/net/Proxy.java
+++ b/core/java/android/net/Proxy.java
@@ -16,32 +16,168 @@
 
 package android.net;
 
-import org.apache.http.HttpHost;
-
 import android.content.ContentResolver;
 import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
 import android.os.SystemProperties;
 import android.provider.Settings;
-import android.util.Log;
 
 import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
 import java.net.URI;
 import java.net.UnknownHostException;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import junit.framework.Assert;
 
+import org.apache.http.HttpHost;
+
 /**
  * A convenience class for accessing the user and default proxy
  * settings.
  */
-final public class Proxy {
+public final class Proxy {
 
     // Set to true to enable extra debugging.
-    static final private boolean DEBUG = false;
+    private static final boolean DEBUG = false;
 
-    static final public String PROXY_CHANGE_ACTION =
+    public static final String PROXY_CHANGE_ACTION =
         "android.intent.action.PROXY_CHANGE";
 
+    private static ReadWriteLock sProxyInfoLock = new ReentrantReadWriteLock();
+
+    private static SettingsObserver sGlobalProxyChangedObserver = null;
+
+    private static ProxySpec sGlobalProxySpec = null;
+
+    // Hostname / IP REGEX validation
+    // Matches blank input, ips, and domain names
+    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;
+
+    private static final String EXCLLIST_REGEXP = "$|^(.?" + NAME_IP_REGEX
+        + ")+(,(.?" + NAME_IP_REGEX + "))*$";
+
+    private static final Pattern EXCLLIST_PATTERN;
+
+    static {
+        HOSTNAME_PATTERN = Pattern.compile(HOSTNAME_REGEXP);
+        EXCLLIST_PATTERN = Pattern.compile(EXCLLIST_REGEXP);
+    }
+
+    private static class ProxySpec {
+        String[] exclusionList = null;
+        InetSocketAddress proxyAddress = null;
+        public ProxySpec() { };
+    }
+
+    private static boolean isURLInExclusionListReadLocked(String url, String[] exclusionList) {
+        if (exclusionList == null) {
+            return false;
+        }
+        Uri u = Uri.parse(url);
+        String urlDomain = u.getHost();
+        // If the domain is defined as ".android.com" or "android.com", we wish to match
+        // http://android.com as well as http://xxx.android.com , but not
+        // http://myandroid.com . This code works out the logic.
+        for (String excludedDomain : exclusionList) {
+            String dotDomain = "." + excludedDomain;
+            if (urlDomain.equals(excludedDomain)) {
+                return true;
+            }
+            if (urlDomain.endsWith(dotDomain)) {
+                return true;
+            }
+        }
+        // No match
+        return false;
+    }
+
+    private static String parseHost(String proxySpec) {
+        int i = proxySpec.indexOf(':');
+        if (i == -1) {
+            if (DEBUG) {
+                Assert.assertTrue(proxySpec.length() == 0);
+            }
+            return null;
+        }
+        return proxySpec.substring(0, i);
+    }
+
+    private static int parsePort(String proxySpec) {
+        int i = proxySpec.indexOf(':');
+        if (i == -1) {
+            if (DEBUG) {
+                Assert.assertTrue(proxySpec.length() == 0);
+            }
+            return -1;
+        }
+        if (DEBUG) {
+            Assert.assertTrue(i < proxySpec.length());
+        }
+        return Integer.parseInt(proxySpec.substring(i+1));
+    }
+
+    /**
+     * Return the proxy object to be used for the URL given as parameter.
+     * @param ctx A Context used to get the settings for the proxy host.
+     * @param url A URL to be accessed. Used to evaluate exclusion list.
+     * @return Proxy (java.net) object containing the host name. If the
+     *         user did not set a hostname it returns the default host.
+     *         A null value means that no host is to be used.
+     * {@hide}
+     */
+    public static final java.net.Proxy getProxy(Context ctx, String url) {
+        sProxyInfoLock.readLock().lock();
+        try {
+            if (sGlobalProxyChangedObserver == null) {
+                registerContentObserversReadLocked(ctx);
+                parseGlobalProxyInfoReadLocked(ctx);
+            }
+            if (sGlobalProxySpec != null) {
+                // Proxy defined - Apply exclusion rules
+                if (isURLInExclusionListReadLocked(url, sGlobalProxySpec.exclusionList)) {
+                    // Return no proxy
+                    return java.net.Proxy.NO_PROXY;
+                }
+                java.net.Proxy retProxy =
+                    new java.net.Proxy(java.net.Proxy.Type.HTTP, sGlobalProxySpec.proxyAddress);
+                sProxyInfoLock.readLock().unlock();
+                if (isLocalHost(url)) {
+                    return java.net.Proxy.NO_PROXY;
+                }
+                sProxyInfoLock.readLock().lock();
+                return retProxy;
+            } else {
+                // If network is WiFi, return no proxy.
+                // Otherwise, return the Mobile Operator proxy.
+                if (!isNetworkWifi(ctx)) {
+                    java.net.Proxy retProxy = getDefaultProxy(url);
+                    sProxyInfoLock.readLock().unlock();
+                    if (isLocalHost(url)) {
+                        return java.net.Proxy.NO_PROXY;
+                    }
+                    sProxyInfoLock.readLock().lock();
+                    return retProxy;
+                } else {
+                    return java.net.Proxy.NO_PROXY;
+                }
+            }
+        } finally {
+            sProxyInfoLock.readLock().unlock();
+        }
+    }
+
+    // TODO: deprecate this function
     /**
      * Return the proxy host set by the user.
      * @param ctx A Context used to get the settings for the proxy host.
@@ -49,58 +185,53 @@
      *         name it returns the default host. A null value means that no
      *         host is to be used.
      */
-    static final public String getHost(Context ctx) {
-        ContentResolver contentResolver = ctx.getContentResolver();
-        Assert.assertNotNull(contentResolver);
-        String host = Settings.Secure.getString(
-                contentResolver,
-                Settings.Secure.HTTP_PROXY);
-        if (host != null) {
-            int i = host.indexOf(':');
-            if (i == -1) {
-                if (DEBUG) {
-                    Assert.assertTrue(host.length() == 0);
-                }
-                return null;
+    public static final String getHost(Context ctx) {
+        sProxyInfoLock.readLock().lock();
+        try {
+            if (sGlobalProxyChangedObserver == null) {
+                registerContentObserversReadLocked(ctx);
+                parseGlobalProxyInfoReadLocked(ctx);
             }
-            return host.substring(0, i);
+            if (sGlobalProxySpec != null) {
+                InetSocketAddress sa = sGlobalProxySpec.proxyAddress;
+                return sa.getHostName();
+            }
+            return getDefaultHost();
+        } finally {
+            sProxyInfoLock.readLock().unlock();
         }
-        return getDefaultHost();
     }
 
+    // TODO: deprecate this function
     /**
      * Return the proxy port set by the user.
      * @param ctx A Context used to get the settings for the proxy port.
      * @return The port number to use or -1 if no proxy is to be used.
      */
-    static final public int getPort(Context ctx) {
-        ContentResolver contentResolver = ctx.getContentResolver();
-        Assert.assertNotNull(contentResolver);
-        String host = Settings.Secure.getString(
-                contentResolver,
-                Settings.Secure.HTTP_PROXY);
-        if (host != null) {
-            int i = host.indexOf(':');
-            if (i == -1) {
-                if (DEBUG) {
-                    Assert.assertTrue(host.length() == 0);
-                }
-                return -1;
+    public static final int getPort(Context ctx) {
+        sProxyInfoLock.readLock().lock();
+        try {
+            if (sGlobalProxyChangedObserver == null) {
+                registerContentObserversReadLocked(ctx);
+                parseGlobalProxyInfoReadLocked(ctx);
             }
-            if (DEBUG) {
-                Assert.assertTrue(i < host.length());
+            if (sGlobalProxySpec != null) {
+                InetSocketAddress sa = sGlobalProxySpec.proxyAddress;
+                return sa.getPort();
             }
-            return Integer.parseInt(host.substring(i+1));
+            return getDefaultPort();
+        } finally {
+            sProxyInfoLock.readLock().unlock();
         }
-        return getDefaultPort();
     }
 
+    // TODO: deprecate this function
     /**
      * Return the default proxy host specified by the carrier.
      * @return String containing the host name or null if there is no proxy for
      * this carrier.
      */
-    static final public String getDefaultHost() {
+    public static final String getDefaultHost() {
         String host = SystemProperties.get("net.gprs.http-proxy");
         if (host != null) {
             Uri u = Uri.parse(host);
@@ -111,12 +242,13 @@
         }
     }
 
+    // TODO: deprecate this function
     /**
      * Return the default proxy port specified by the carrier.
      * @return The port number to be used with the proxy host or -1 if there is
      * no proxy for this carrier.
      */
-    static final public int getDefaultPort() {
+    public static final int getDefaultPort() {
         String host = SystemProperties.get("net.gprs.http-proxy");
         if (host != null) {
             Uri u = Uri.parse(host);
@@ -126,6 +258,20 @@
         }
     }
 
+    private static final java.net.Proxy getDefaultProxy(String url) {
+        // TODO: This will go away when information is collected from ConnectivityManager...
+        // There are broadcast of network proxies, so they are parse manually.
+        String host = SystemProperties.get("net.gprs.http-proxy");
+        if (host != null) {
+            Uri u = Uri.parse(host);
+            return new java.net.Proxy(java.net.Proxy.Type.HTTP,
+                    new InetSocketAddress(u.getHost(), u.getPort()));
+        } else {
+            return java.net.Proxy.NO_PROXY;
+        }
+    }
+
+    // TODO: remove this function / deprecate
     /**
      * Returns the preferred proxy to be used by clients. This is a wrapper
      * around {@link android.net.Proxy#getHost()}. Currently no proxy will
@@ -138,26 +284,23 @@
      * android.permission.ACCESS_NETWORK_STATE
      * @return The preferred proxy to be used by clients, or null if there
      * is no proxy.
-     *
      * {@hide}
      */
-    static final public HttpHost getPreferredHttpHost(Context context,
+    public static final HttpHost getPreferredHttpHost(Context context,
             String url) {
-        if (!isLocalHost(url) && !isNetworkWifi(context)) {
-            final String proxyHost = Proxy.getHost(context);
-            if (proxyHost != null) {
-                return new HttpHost(proxyHost, Proxy.getPort(context), "http");
-            }
+        java.net.Proxy prefProxy = getProxy(context, url);
+        if (prefProxy.equals(java.net.Proxy.NO_PROXY)) {
+            return null;
+        } else {
+            InetSocketAddress sa = (InetSocketAddress)prefProxy.address();
+            return new HttpHost(sa.getHostName(), sa.getPort(), "http");
         }
-
-        return null;
     }
 
-    static final private boolean isLocalHost(String url) {
+    private static final boolean isLocalHost(String url) {
         if (url == null) {
             return false;
         }
-
         try {
             final URI uri = URI.create(url);
             final String host = uri.getHost();
@@ -174,15 +317,13 @@
         } catch (IllegalArgumentException iex) {
             // Ignore (URI.create)
         }
-
         return false;
     }
 
-    static final private boolean isNetworkWifi(Context context) {
+    private static final boolean isNetworkWifi(Context context) {
         if (context == null) {
             return false;
         }
-
         final ConnectivityManager connectivity = (ConnectivityManager)
             context.getSystemService(Context.CONNECTIVITY_SERVICE);
         if (connectivity != null) {
@@ -192,7 +333,120 @@
                 return true;
             }
         }
-
         return false;
     }
-};
+
+    private static class SettingsObserver extends ContentObserver {
+
+        private Context mContext;
+
+        SettingsObserver(Context ctx) {
+            super(new Handler());
+            mContext = ctx;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            sProxyInfoLock.readLock().lock();
+            parseGlobalProxyInfoReadLocked(mContext);
+            sProxyInfoLock.readLock().unlock();
+        }
+    }
+
+    private static final void registerContentObserversReadLocked(Context ctx) {
+        Uri uriGlobalProxy = Settings.Secure.getUriFor(Settings.Secure.HTTP_PROXY);
+        Uri uriGlobalExclList =
+            Settings.Secure.getUriFor(Settings.Secure.HTTP_PROXY_EXCLUSION_LIST);
+
+        // No lock upgrading (from read to write) allowed
+        sProxyInfoLock.readLock().unlock();
+        sProxyInfoLock.writeLock().lock();
+        sGlobalProxyChangedObserver = new SettingsObserver(ctx);
+        // Downgrading locks (from write to read) is allowed
+        sProxyInfoLock.readLock().lock();
+        sProxyInfoLock.writeLock().unlock();
+        ctx.getContentResolver().registerContentObserver(uriGlobalProxy, false,
+                sGlobalProxyChangedObserver);
+        ctx.getContentResolver().registerContentObserver(uriGlobalExclList, false,
+                sGlobalProxyChangedObserver);
+    }
+
+    private static final void parseGlobalProxyInfoReadLocked(Context ctx) {
+        ContentResolver contentResolver = ctx.getContentResolver();
+        String proxyHost =  Settings.Secure.getString(
+                contentResolver,
+                Settings.Secure.HTTP_PROXY);
+        if (proxyHost == null) {
+            return;
+        }
+        String exclusionListSpec = Settings.Secure.getString(
+                contentResolver,
+                Settings.Secure.HTTP_PROXY_EXCLUSION_LIST);
+        String host = parseHost(proxyHost);
+        int port = parsePort(proxyHost);
+        if (proxyHost != null) {
+            sGlobalProxySpec = new ProxySpec();
+            sGlobalProxySpec.proxyAddress = new InetSocketAddress(host, port);
+            if (exclusionListSpec != null) {
+                String[] exclusionListEntries = exclusionListSpec.toLowerCase().split(",");
+                String[] processedEntries = new String[exclusionListEntries.length];
+                for (int i = 0; i < exclusionListEntries.length; i++) {
+                    String entry = exclusionListEntries[i].trim();
+                    if (entry.startsWith(".")) {
+                        entry = entry.substring(1);
+                    }
+                    processedEntries[i] = entry;
+                }
+                sProxyInfoLock.readLock().unlock();
+                sProxyInfoLock.writeLock().lock();
+                sGlobalProxySpec.exclusionList = processedEntries;
+            } else {
+                sProxyInfoLock.readLock().unlock();
+                sProxyInfoLock.writeLock().lock();
+                sGlobalProxySpec.exclusionList = null;
+            }
+        } else {
+            sProxyInfoLock.readLock().unlock();
+            sProxyInfoLock.writeLock().lock();
+            sGlobalProxySpec = null;
+        }
+        sProxyInfoLock.readLock().lock();
+        sProxyInfoLock.writeLock().unlock();
+    }
+
+    /**
+     * Validate syntax of hostname, port and exclusion list entries
+     * {@hide}
+     */
+    public static void validate(String hostname, String port, String exclList) {
+        Matcher match = HOSTNAME_PATTERN.matcher(hostname);
+        Matcher listMatch = EXCLLIST_PATTERN.matcher(exclList);
+
+        if (!match.matches()) {
+            throw new IllegalArgumentException();
+        }
+
+        if (!listMatch.matches()) {
+            throw new IllegalArgumentException();
+        }
+
+        if (hostname.length() > 0 && port.length() == 0) {
+            throw new IllegalArgumentException();
+        }
+
+        if (port.length() > 0) {
+            if (hostname.length() == 0) {
+                throw new IllegalArgumentException();
+            }
+            int portVal = -1;
+            try {
+                portVal = Integer.parseInt(port);
+            } catch (NumberFormatException ex) {
+                throw new IllegalArgumentException();
+            }
+            if (portVal <= 0 || portVal > 0xFFFF) {
+                throw new IllegalArgumentException();
+            }
+        }
+    }
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 4ec5363..a4f9f4e 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2321,11 +2321,26 @@
         public static final String DISABLED_SYSTEM_INPUT_METHODS = "disabled_system_input_methods";
 
         /**
-         * Host name and port for a user-selected proxy.
+         * Host name and port for global proxy.
          */
         public static final String HTTP_PROXY = "http_proxy";
 
         /**
+         * Exclusion list for global proxy. This string contains a list of comma-separated
+         * domains where the global proxy does not apply. Domains should be listed in a comma-
+         * separated list. Example of acceptable formats: ".domain1.com,my.domain2.com"
+         * @hide
+         */
+        public static final String HTTP_PROXY_EXCLUSION_LIST = "http_proxy_exclusion_list";
+
+        /**
+         * Enables the UI setting to allow the user to specify the global HTTP proxy
+         * and associated exclusion list.
+         * @hide
+         */
+        public static final String SET_GLOBAL_HTTP_PROXY = "set_global_http_proxy";
+
+        /**
          * Whether the package installer should allow installation of apps downloaded from
          * sources other than the Android Market (vending machine).
          *