Merge "Connect metered DHCP hint for Wi-Fi networks."
diff --git a/core/java/android/net/DhcpInfoInternal.java b/core/java/android/net/DhcpInfoInternal.java
index 7ab8047..c87c34b 100644
--- a/core/java/android/net/DhcpInfoInternal.java
+++ b/core/java/android/net/DhcpInfoInternal.java
@@ -142,6 +142,14 @@
         }
     }
 
+    /**
+     * Test if this DHCP lease includes vendor hint that network link is
+     * metered, and sensitive to heavy data transfers.
+     */
+    public boolean hasMeteredHint() {
+        return "ANDROID_METERED".equals(vendorInfo);
+    }
+
     public String toString() {
         String routeString = "";
         for (RouteInfo route : mRoutes) routeString += route.toString() + " | ";
diff --git a/services/java/com/android/server/net/NetworkPolicyManagerService.java b/services/java/com/android/server/net/NetworkPolicyManagerService.java
index 1e17067..52d5019 100644
--- a/services/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -30,6 +30,8 @@
 import static android.net.ConnectivityManager.TYPE_MOBILE;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.ConnectivityManager.TYPE_WIMAX;
+import static android.net.ConnectivityManager.isNetworkTypeMobile;
+import static android.net.NetworkPolicy.CYCLE_NONE;
 import static android.net.NetworkPolicy.LIMIT_DISABLED;
 import static android.net.NetworkPolicy.SNOOZE_NEVER;
 import static android.net.NetworkPolicy.WARNING_DISABLED;
@@ -48,6 +50,14 @@
 import static android.net.NetworkTemplate.MATCH_WIFI;
 import static android.net.NetworkTemplate.buildTemplateMobileAll;
 import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.net.wifi.WifiInfo.removeDoubleQuotes;
+import static android.net.wifi.WifiManager.CHANGE_REASON_ADDED;
+import static android.net.wifi.WifiManager.CHANGE_REASON_REMOVED;
+import static android.net.wifi.WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION;
+import static android.net.wifi.WifiManager.EXTRA_CHANGE_REASON;
+import static android.net.wifi.WifiManager.EXTRA_NETWORK_INFO;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_CONFIGURATION;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_INFO;
 import static android.telephony.TelephonyManager.SIM_STATE_READY;
 import static android.text.format.DateUtils.DAY_IN_MILLIS;
 import static com.android.internal.util.ArrayUtils.appendInt;
@@ -84,10 +94,14 @@
 import android.net.INetworkPolicyManager;
 import android.net.INetworkStatsService;
 import android.net.NetworkIdentity;
+import android.net.NetworkInfo;
 import android.net.NetworkPolicy;
 import android.net.NetworkQuotaInfo;
 import android.net.NetworkState;
 import android.net.NetworkTemplate;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
 import android.os.Binder;
 import android.os.Environment;
 import android.os.Handler;
@@ -355,6 +369,17 @@
         mContext.registerReceiver(mSnoozeWarningReceiver, snoozeWarningFilter,
                 MANAGE_NETWORK_POLICY, mHandler);
 
+        // listen for configured wifi networks to be removed
+        final IntentFilter wifiConfigFilter = new IntentFilter(CONFIGURED_NETWORKS_CHANGED_ACTION);
+        mContext.registerReceiver(
+                mWifiConfigReceiver, wifiConfigFilter, CONNECTIVITY_INTERNAL, mHandler);
+
+        // listen for wifi state changes to catch metered hint
+        final IntentFilter wifiStateFilter = new IntentFilter(
+                WifiManager.NETWORK_STATE_CHANGED_ACTION);
+        mContext.registerReceiver(
+                mWifiStateReceiver, wifiStateFilter, CONNECTIVITY_INTERNAL, mHandler);
+
     }
 
     private IProcessObserver mProcessObserver = new IProcessObserver.Stub() {
@@ -463,6 +488,73 @@
     };
 
     /**
+     * Receiver that watches for {@link WifiConfiguration} to be changed.
+     */
+    private BroadcastReceiver mWifiConfigReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // on background handler thread, and verified CONNECTIVITY_INTERNAL
+            // permission above.
+
+            final int reason = intent.getIntExtra(EXTRA_CHANGE_REASON, CHANGE_REASON_ADDED);
+            if (reason == CHANGE_REASON_REMOVED) {
+                final WifiConfiguration config = intent.getParcelableExtra(
+                        EXTRA_WIFI_CONFIGURATION);
+                final NetworkTemplate template = NetworkTemplate.buildTemplateWifi(
+                        removeDoubleQuotes(config.SSID));
+                synchronized (mRulesLock) {
+                    if (mNetworkPolicy.containsKey(template)) {
+                        mNetworkPolicy.remove(template);
+                        writePolicyLocked();
+                    }
+                }
+            }
+        }
+    };
+
+    /**
+     * Receiver that watches {@link WifiInfo} state changes to infer metered
+     * state. Ignores hints when policy is user-defined.
+     */
+    private BroadcastReceiver mWifiStateReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // on background handler thread, and verified CONNECTIVITY_INTERNAL
+            // permission above.
+
+            // ignore when not connected
+            final NetworkInfo netInfo = intent.getParcelableExtra(EXTRA_NETWORK_INFO);
+            if (!netInfo.isConnected()) return;
+
+            final WifiInfo info = intent.getParcelableExtra(EXTRA_WIFI_INFO);
+            final boolean meteredHint = info.getMeteredHint();
+
+            final NetworkTemplate template = NetworkTemplate.buildTemplateWifi(
+                    removeDoubleQuotes(info.getSSID()));
+            synchronized (mRulesLock) {
+                NetworkPolicy policy = mNetworkPolicy.get(template);
+                if (policy == null && meteredHint) {
+                    // policy doesn't exist, and AP is hinting that it's
+                    // metered: create an inferred policy.
+                    policy = new NetworkPolicy(template, CYCLE_NONE, Time.TIMEZONE_UTC,
+                            WARNING_DISABLED, LIMIT_DISABLED, SNOOZE_NEVER, SNOOZE_NEVER,
+                            meteredHint, true);
+                    addNetworkPolicyLocked(policy);
+
+                } else if (policy != null && policy.inferred) {
+                    // policy exists, and was inferred: update its current
+                    // metered state.
+                    policy.metered = meteredHint;
+
+                    // since this is inferred for each wifi session, just update
+                    // rules without persisting.
+                    updateNetworkRulesLocked();
+                }
+            }
+        }
+    };
+
+    /**
      * Observer that watches for {@link INetworkManagementService} alerts.
      */
     private INetworkManagementEventObserver mAlertObserver = new NetworkAlertObserver() {
@@ -974,9 +1066,9 @@
             final String cycleTimezone = time.timezone;
 
             final NetworkTemplate template = buildTemplateMobileAll(subscriberId);
-            mNetworkPolicy.put(template, new NetworkPolicy(template, cycleDay, cycleTimezone,
-                    warningBytes, LIMIT_DISABLED, SNOOZE_NEVER, SNOOZE_NEVER, true, true));
-            writePolicyLocked();
+            final NetworkPolicy policy = new NetworkPolicy(template, cycleDay, cycleTimezone,
+                    warningBytes, LIMIT_DISABLED, SNOOZE_NEVER, SNOOZE_NEVER, true, true);
+            addNetworkPolicyLocked(policy);
         }
     }
 
@@ -1269,6 +1361,15 @@
         }
     }
 
+    private void addNetworkPolicyLocked(NetworkPolicy policy) {
+        mNetworkPolicy.put(policy.template, policy);
+
+        updateNetworkEnabledLocked();
+        updateNetworkRulesLocked();
+        updateNotificationsLocked();
+        writePolicyLocked();
+    }
+
     @Override
     public NetworkPolicy[] getNetworkPolicies() {
         mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
@@ -1402,6 +1503,10 @@
         if (policy != null) {
             return policy.metered;
         } else {
+            final int type = state.networkInfo.getType();
+            if (isNetworkTypeMobile(type) || type == TYPE_WIMAX) {
+                return true;
+            }
             return false;
         }
     }
diff --git a/wifi/java/android/net/wifi/WifiInfo.java b/wifi/java/android/net/wifi/WifiInfo.java
index 7bb927b..30e4a20 100644
--- a/wifi/java/android/net/wifi/WifiInfo.java
+++ b/wifi/java/android/net/wifi/WifiInfo.java
@@ -68,9 +68,14 @@
     private int mLinkSpeed;
 
     private InetAddress mIpAddress;
-
     private String mMacAddress;
 
+    /**
+     * Flag indicating that AP has hinted that upstream connection is metered,
+     * and sensitive to heavy data transfers.
+     */
+    private boolean mMeteredHint;
+
     WifiInfo() {
         mSSID = null;
         mBSSID = null;
@@ -96,6 +101,7 @@
             mLinkSpeed = source.mLinkSpeed;
             mIpAddress = source.mIpAddress;
             mMacAddress = source.mMacAddress;
+            mMeteredHint = source.mMeteredHint;
         }
     }
 
@@ -168,6 +174,16 @@
         return mMacAddress;
     }
 
+    /** {@hide} */
+    public void setMeteredHint(boolean meteredHint) {
+        mMeteredHint = meteredHint;
+    }
+
+    /** {@hide} */
+    public boolean getMeteredHint() {
+        return mMeteredHint;
+    }
+
     void setNetworkId(int id) {
         mNetworkId = id;
     }
@@ -248,6 +264,15 @@
         }
     }
 
+    /** {@hide} */
+    public static String removeDoubleQuotes(String string) {
+        final int length = string.length();
+        if ((length > 1) && (string.charAt(0) == '"') && (string.charAt(length - 1) == '"')) {
+            return string.substring(1, length - 1);
+        }
+        return string;
+    }
+
     @Override
     public String toString() {
         StringBuffer sb = new StringBuffer();
@@ -260,7 +285,8 @@
             append(mSupplicantState == null ? none : mSupplicantState).
             append(", RSSI: ").append(mRssi).
             append(", Link speed: ").append(mLinkSpeed).
-            append(", Net ID: ").append(mNetworkId);
+            append(", Net ID: ").append(mNetworkId).
+            append(", Metered hint: ").append(mMeteredHint);
 
         return sb.toString();
     }
@@ -284,6 +310,7 @@
         dest.writeString(getSSID());
         dest.writeString(mBSSID);
         dest.writeString(mMacAddress);
+        dest.writeInt(mMeteredHint ? 1 : 0);
         mSupplicantState.writeToParcel(dest, flags);
     }
 
@@ -303,6 +330,7 @@
                 info.setSSID(in.readString());
                 info.mBSSID = in.readString();
                 info.mMacAddress = in.readString();
+                info.mMeteredHint = in.readInt() != 0;
                 info.mSupplicantState = SupplicantState.CREATOR.createFromParcel(in);
                 return info;
             }
diff --git a/wifi/java/android/net/wifi/WifiStateMachine.java b/wifi/java/android/net/wifi/WifiStateMachine.java
index 2f14098..705e3c7 100644
--- a/wifi/java/android/net/wifi/WifiStateMachine.java
+++ b/wifi/java/android/net/wifi/WifiStateMachine.java
@@ -1642,6 +1642,7 @@
         mWifiInfo.setNetworkId(WifiConfiguration.INVALID_NETWORK_ID);
         mWifiInfo.setRssi(MIN_RSSI);
         mWifiInfo.setLinkSpeed(-1);
+        mWifiInfo.setMeteredHint(false);
 
         setNetworkDetailedState(DetailedState.DISCONNECTED);
         mWifiConfigStore.updateStatus(mLastNetworkId, DetailedState.DISCONNECTED);
@@ -1713,6 +1714,7 @@
         mWifiConfigStore.setIpConfiguration(mLastNetworkId, dhcpInfoInternal);
         InetAddress addr = NetworkUtils.numericToInetAddress(dhcpInfoInternal.ipAddress);
         mWifiInfo.setInetAddress(addr);
+        mWifiInfo.setMeteredHint(dhcpInfoInternal.hasMeteredHint());
         if (getNetworkDetailedState() == DetailedState.CONNECTED) {
             //DHCP renewal in connected state
             LinkProperties linkProperties = dhcpInfoInternal.makeLinkProperties();
@@ -1735,6 +1737,7 @@
         loge("IP configuration failed");
 
         mWifiInfo.setInetAddress(null);
+        mWifiInfo.setMeteredHint(false);
         /**
          * If we've exceeded the maximum number of retries for DHCP
          * to a given network, disable the network