Offer to "merge" subscribers for data usage.

There are some cases where multiple subscriber identities (IMSI)
should be treated as "merged together" from a data usage
perspective.  This is done by extending the template used for
matching purposes to support multiple subscribers.

Then, when we query historical usage or set network policies, we
normalize the matching template to merge to any other identities
that should be included.  When normalizing, the "lowest" identity
is always used for equality and storage purposes, which allows
identities to come and go over time.

This change also fixes data usage recording for multi-SIM devices
by passing along the concrete subscriber identity for each network
interface.  Also correctly create default policies for multi-SIM
devices.  This change also drops setPolicyDataEnable() until it can
be wired up to the right underlying NetworkAgent.  (This means we
still bring up the network, and then rely on iptables rules to block
traffic when over the limit, instead of proactively disabling the
connection.)

Bug: 18012787
Change-Id: If6acf32009fdfea2b836f5aff8e2f3e5e0248b4a
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index 8021210..d9921a6 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -68,9 +68,6 @@
 
     boolean requestRouteToHostAddress(int networkType, in byte[] hostAddress);
 
-    /** Policy control over specific {@link NetworkStateTracker}. */
-    void setPolicyDataEnable(int networkType, boolean enabled);
-
     int tether(String iface);
 
     int untether(String iface);
diff --git a/core/java/android/net/NetworkIdentity.java b/core/java/android/net/NetworkIdentity.java
index d36707e..6864749 100644
--- a/core/java/android/net/NetworkIdentity.java
+++ b/core/java/android/net/NetworkIdentity.java
@@ -25,6 +25,7 @@
 import android.net.wifi.WifiManager;
 import android.os.Build;
 import android.telephony.TelephonyManager;
+import android.util.Slog;
 
 import java.util.Objects;
 
@@ -35,6 +36,8 @@
  * @hide
  */
 public class NetworkIdentity implements Comparable<NetworkIdentity> {
+    private static final String TAG = "NetworkIdentity";
+
     /**
      * When enabled, combine all {@link #mSubType} together under
      * {@link #SUBTYPE_COMBINED}.
@@ -133,6 +136,18 @@
     }
 
     /**
+     * Scrub given IMSI on production builds.
+     */
+    public static String[] scrubSubscriberId(String[] subscriberId) {
+        if (subscriberId == null) return null;
+        final String[] res = new String[subscriberId.length];
+        for (int i = 0; i < res.length; i++) {
+            res[i] = NetworkIdentity.scrubSubscriberId(subscriberId[i]);
+        }
+        return res;
+    }
+
+    /**
      * Build a {@link NetworkIdentity} from the given {@link NetworkState},
      * assuming that any mobile networks are using the current IMSI.
      */
@@ -140,23 +155,18 @@
         final int type = state.networkInfo.getType();
         final int subType = state.networkInfo.getSubtype();
 
-        // TODO: consider moving subscriberId over to LinkCapabilities, so it
-        // comes from an authoritative source.
-
         String subscriberId = null;
         String networkId = null;
         boolean roaming = false;
 
         if (isNetworkTypeMobile(type)) {
-            final TelephonyManager telephony = (TelephonyManager) context.getSystemService(
-                    Context.TELEPHONY_SERVICE);
-            roaming = telephony.isNetworkRoaming();
-            if (state.subscriberId != null) {
-                subscriberId = state.subscriberId;
-            } else {
-                subscriberId = telephony.getSubscriberId();
+            if (state.subscriberId == null) {
+                Slog.w(TAG, "Active mobile network without subscriber!");
             }
 
+            subscriberId = state.subscriberId;
+            roaming = state.networkInfo.isRoaming();
+
         } else if (type == TYPE_WIFI) {
             if (state.networkId != null) {
                 networkId = state.networkId;
diff --git a/core/java/android/net/NetworkMisc.java b/core/java/android/net/NetworkMisc.java
index 5d2a43d..b92c9e3 100644
--- a/core/java/android/net/NetworkMisc.java
+++ b/core/java/android/net/NetworkMisc.java
@@ -20,15 +20,18 @@
 import android.os.Parcelable;
 
 /**
- * A grab-bag of information (metadata, policies, properties, etc) about a {@link Network}.
+ * A grab-bag of information (metadata, policies, properties, etc) about a
+ * {@link Network}. Since this contains PII, it should not be sent outside the
+ * system.
  *
  * @hide
  */
 public class NetworkMisc implements Parcelable {
 
     /**
-     * If the {@link Network} is a VPN, whether apps are allowed to bypass the VPN. This is set by
-     * a {@link VpnService} and used by {@link ConnectivityService} when creating a VPN.
+     * If the {@link Network} is a VPN, whether apps are allowed to bypass the
+     * VPN. This is set by a {@link VpnService} and used by
+     * {@link ConnectivityManager} when creating a VPN.
      */
     public boolean allowBypass;
 
@@ -41,6 +44,11 @@
      */
     public boolean explicitlySelected;
 
+    /**
+     * For mobile networks, this is the subscriber ID (such as IMSI).
+     */
+    public String subscriberId;
+
     public NetworkMisc() {
     }
 
@@ -48,6 +56,7 @@
         if (nm != null) {
             allowBypass = nm.allowBypass;
             explicitlySelected = nm.explicitlySelected;
+            subscriberId = nm.subscriberId;
         }
     }
 
@@ -60,6 +69,7 @@
     public void writeToParcel(Parcel out, int flags) {
         out.writeInt(allowBypass ? 1 : 0);
         out.writeInt(explicitlySelected ? 1 : 0);
+        out.writeString(subscriberId);
     }
 
     public static final Creator<NetworkMisc> CREATOR = new Creator<NetworkMisc>() {
@@ -68,6 +78,7 @@
             NetworkMisc networkMisc = new NetworkMisc();
             networkMisc.allowBypass = in.readInt() != 0;
             networkMisc.explicitlySelected = in.readInt() != 0;
+            networkMisc.subscriberId = in.readString();
             return networkMisc;
         }
 
diff --git a/core/java/android/net/NetworkPolicy.java b/core/java/android/net/NetworkPolicy.java
index 10c686b..e88bc26 100644
--- a/core/java/android/net/NetworkPolicy.java
+++ b/core/java/android/net/NetworkPolicy.java
@@ -35,7 +35,7 @@
     public static final long LIMIT_DISABLED = -1;
     public static final long SNOOZE_NEVER = -1;
 
-    public final NetworkTemplate template;
+    public NetworkTemplate template;
     public int cycleDay;
     public String cycleTimezone;
     public long warningBytes;
diff --git a/core/java/android/net/NetworkState.java b/core/java/android/net/NetworkState.java
index d26c70d..933287f 100644
--- a/core/java/android/net/NetworkState.java
+++ b/core/java/android/net/NetworkState.java
@@ -30,16 +30,10 @@
     public final LinkProperties linkProperties;
     public final NetworkCapabilities networkCapabilities;
     public final Network network;
-    /** Currently only used by testing. */
     public final String subscriberId;
     public final String networkId;
 
     public NetworkState(NetworkInfo networkInfo, LinkProperties linkProperties,
-            NetworkCapabilities networkCapabilities, Network network) {
-        this(networkInfo, linkProperties, networkCapabilities, network, null, null);
-    }
-
-    public NetworkState(NetworkInfo networkInfo, LinkProperties linkProperties,
             NetworkCapabilities networkCapabilities, Network network, String subscriberId,
             String networkId) {
         this.networkInfo = networkInfo;
@@ -85,5 +79,4 @@
             return new NetworkState[size];
         }
     };
-
 }
diff --git a/core/java/android/net/NetworkTemplate.java b/core/java/android/net/NetworkTemplate.java
index b839e0a..6cfab92 100644
--- a/core/java/android/net/NetworkTemplate.java
+++ b/core/java/android/net/NetworkTemplate.java
@@ -22,7 +22,6 @@
 import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
 import static android.net.ConnectivityManager.TYPE_WIMAX;
 import static android.net.NetworkIdentity.COMBINE_SUBTYPE_ENABLED;
-import static android.net.NetworkIdentity.scrubSubscriberId;
 import static android.net.wifi.WifiInfo.removeDoubleQuotes;
 import static android.telephony.TelephonyManager.NETWORK_CLASS_2_G;
 import static android.telephony.TelephonyManager.NETWORK_CLASS_3_G;
@@ -36,7 +35,9 @@
 import android.os.Parcelable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
 
+import java.util.Arrays;
 import java.util.Objects;
 
 /**
@@ -146,17 +147,35 @@
 
     private final int mMatchRule;
     private final String mSubscriberId;
+
+    /**
+     * Ugh, templates are designed to target a single subscriber, but we might
+     * need to match several "merged" subscribers. These are the subscribers
+     * that should be considered to match this template.
+     * <p>
+     * Since the merge set is dynamic, it should <em>not</em> be persisted or
+     * used for determining equality.
+     */
+    private final String[] mMatchSubscriberIds;
+
     private final String mNetworkId;
 
     public NetworkTemplate(int matchRule, String subscriberId, String networkId) {
+        this(matchRule, subscriberId, new String[] { subscriberId }, networkId);
+    }
+
+    public NetworkTemplate(int matchRule, String subscriberId, String[] matchSubscriberIds,
+            String networkId) {
         mMatchRule = matchRule;
         mSubscriberId = subscriberId;
+        mMatchSubscriberIds = matchSubscriberIds;
         mNetworkId = networkId;
     }
 
     private NetworkTemplate(Parcel in) {
         mMatchRule = in.readInt();
         mSubscriberId = in.readString();
+        mMatchSubscriberIds = in.createStringArray();
         mNetworkId = in.readString();
     }
 
@@ -164,6 +183,7 @@
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeInt(mMatchRule);
         dest.writeString(mSubscriberId);
+        dest.writeStringArray(mMatchSubscriberIds);
         dest.writeString(mNetworkId);
     }
 
@@ -177,7 +197,12 @@
         final StringBuilder builder = new StringBuilder("NetworkTemplate: ");
         builder.append("matchRule=").append(getMatchRuleName(mMatchRule));
         if (mSubscriberId != null) {
-            builder.append(", subscriberId=").append(scrubSubscriberId(mSubscriberId));
+            builder.append(", subscriberId=").append(
+                    NetworkIdentity.scrubSubscriberId(mSubscriberId));
+        }
+        if (mMatchSubscriberIds != null) {
+            builder.append(", matchSubscriberIds=").append(
+                    Arrays.toString(NetworkIdentity.scrubSubscriberId(mMatchSubscriberIds)));
         }
         if (mNetworkId != null) {
             builder.append(", networkId=").append(mNetworkId);
@@ -201,6 +226,18 @@
         return false;
     }
 
+    public boolean isMatchRuleMobile() {
+        switch (mMatchRule) {
+            case MATCH_MOBILE_3G_LOWER:
+            case MATCH_MOBILE_4G:
+            case MATCH_MOBILE_ALL:
+            case MATCH_MOBILE_WILDCARD:
+                return true;
+            default:
+                return false;
+        }
+    }
+
     public int getMatchRule() {
         return mMatchRule;
     }
@@ -247,8 +284,9 @@
             // TODO: consider matching against WiMAX subscriber identity
             return true;
         } else {
-            return ((sForceAllNetworkTypes || contains(DATA_USAGE_NETWORK_TYPES, ident.mType))
-                    && Objects.equals(mSubscriberId, ident.mSubscriberId));
+            final boolean matchesType = (sForceAllNetworkTypes
+                    || contains(DATA_USAGE_NETWORK_TYPES, ident.mType));
+            return matchesType && ArrayUtils.contains(mMatchSubscriberIds, ident.mSubscriberId);
         }
     }
 
@@ -368,6 +406,27 @@
         }
     }
 
+    /**
+     * Examine the given template and normalize if it refers to a "merged"
+     * mobile subscriber. We pick the "lowest" merged subscriber as the primary
+     * for key purposes, and expand the template to match all other merged
+     * subscribers.
+     * <p>
+     * For example, given an incoming template matching B, and the currently
+     * active merge set [A,B], we'd return a new template that primarily matches
+     * A, but also matches B.
+     */
+    public static NetworkTemplate normalize(NetworkTemplate template, String[] merged) {
+        if (template.isMatchRuleMobile() && ArrayUtils.contains(merged, template.mSubscriberId)) {
+            // Requested template subscriber is part of the merge group; return
+            // a template that matches all merged subscribers.
+            return new NetworkTemplate(template.mMatchRule, merged[0], merged,
+                    template.mNetworkId);
+        } else {
+            return template;
+        }
+    }
+
     public static final Creator<NetworkTemplate> CREATOR = new Creator<NetworkTemplate>() {
         @Override
         public NetworkTemplate createFromParcel(Parcel in) {
diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java
index 8e786da..f908fcb 100644
--- a/core/java/com/android/internal/util/ArrayUtils.java
+++ b/core/java/com/android/internal/util/ArrayUtils.java
@@ -128,6 +128,20 @@
     }
 
     /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static boolean isEmpty(int[] array) {
+        return array == null || array.length == 0;
+    }
+
+    /**
+     * Checks if given array is null or has zero elements.
+     */
+    public static boolean isEmpty(long[] array) {
+        return array == null || array.length == 0;
+    }
+
+    /**
      * Checks that value is present as at least one of the elements of the array.
      * @param array the array to check in
      * @param value the value to check for
@@ -157,6 +171,7 @@
      * Test if all {@code check} items are contained in {@code array}.
      */
     public static <T> boolean containsAll(T[] array, T[] check) {
+        if (check == null) return true;
         for (T checkItem : check) {
             if (!contains(array, checkItem)) {
                 return false;