Support GID1 for VVM configs

This CL allows VVM configs to be specifed with Group identifier, which takes precedence over configs with only MCC/MNC.

TelephonyVvmConfigManger is renamed to DialerVvmConfigManger. It was created when VVM was still in telephony, and need to separate itself from the CarrierConfig app.

Bug: 72666573
Test: Unit tests
PiperOrigin-RevId: 184924155
Change-Id: Ic71e99ed2b3015eed87dfb7e111538ff3b744206
diff --git a/java/com/android/voicemail/impl/CarrierIdentifier.java b/java/com/android/voicemail/impl/CarrierIdentifier.java
new file mode 100644
index 0000000..82b6a24
--- /dev/null
+++ b/java/com/android/voicemail/impl/CarrierIdentifier.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build.VERSION_CODES;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.google.auto.value.AutoValue;
+
+/** Identifies a carrier. */
+@AutoValue
+@TargetApi(VERSION_CODES.O)
+@SuppressWarnings("missingpermission")
+public abstract class CarrierIdentifier {
+
+  public abstract String mccMnc();
+
+  /**
+   * Group ID Level 1. Used to identify MVNO(Mobile Virtual Network Operators) who subleases other
+   * carrier's network and share their mccMnc. MVNO should have a GID1 different from the host.
+   */
+  public abstract String gid1();
+
+  /** Builder for the matcher */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder setMccMnc(String mccMnc);
+
+    public abstract Builder setGid1(String gid1);
+
+    public abstract CarrierIdentifier build();
+  }
+
+  public static Builder builder() {
+    return new AutoValue_CarrierIdentifier.Builder().setGid1("");
+  }
+
+  public static CarrierIdentifier forHandle(
+      Context context, PhoneAccountHandle phoneAccountHandle) {
+    TelephonyManager telephonyManager =
+        context
+            .getSystemService(TelephonyManager.class)
+            .createForPhoneAccountHandle(phoneAccountHandle);
+    if (telephonyManager == null) {
+      throw new IllegalArgumentException("Invalid PhoneAccountHandle");
+    }
+    String gid1 = telephonyManager.getGroupIdLevel1();
+    if (gid1 == null) {
+      gid1 = "";
+    }
+
+    return builder().setMccMnc(telephonyManager.getSimOperator()).setGid1(gid1).build();
+  }
+}
diff --git a/java/com/android/voicemail/impl/CarrierIdentifierMatcher.java b/java/com/android/voicemail/impl/CarrierIdentifierMatcher.java
new file mode 100644
index 0000000..d7c28fe
--- /dev/null
+++ b/java/com/android/voicemail/impl/CarrierIdentifierMatcher.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+
+/**
+ * Matches a {@link CarrierIdentifier}. Full equality check on CarrierIdentifiers is often unfit
+ * because non-MVNO carriers usually just specify the {@link CarrierIdentifier#mccMnc()} while their
+ * {@link CarrierIdentifier#gid1()} could be anything. This matcher ignore fields that are not
+ * specified in the matcher.
+ */
+@AutoValue
+public abstract class CarrierIdentifierMatcher {
+
+  public abstract String mccMnc();
+
+  public abstract Optional<String> gid1();
+
+  public static Builder builder() {
+    return new AutoValue_CarrierIdentifierMatcher.Builder();
+  }
+
+  /** Builder for the matcher */
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder setMccMnc(String mccMnc);
+
+    public abstract Builder setGid1(String gid1);
+
+    public abstract CarrierIdentifierMatcher build();
+  }
+
+  public boolean matches(CarrierIdentifier carrierIdentifier) {
+    if (!mccMnc().equals(carrierIdentifier.mccMnc())) {
+      return false;
+    }
+    if (gid1().isPresent()) {
+      if (!gid1().get().equals(carrierIdentifier.gid1())) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/java/com/android/voicemail/impl/DialerVvmConfigManager.java b/java/com/android/voicemail/impl/DialerVvmConfigManager.java
new file mode 100644
index 0000000..7fa960e
--- /dev/null
+++ b/java/com/android/voicemail/impl/DialerVvmConfigManager.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.PersistableBundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+import com.android.dialer.configprovider.ConfigProviderBindings;
+import com.android.voicemail.impl.utils.XmlUtils;
+import com.google.common.collect.ComparisonChain;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/** Load and caches dialer vvm config from res/xml/vvm_config.xml */
+public class DialerVvmConfigManager {
+  private static class ConfigEntry implements Comparable<ConfigEntry> {
+
+    final CarrierIdentifierMatcher matcher;
+    final PersistableBundle config;
+
+    ConfigEntry(CarrierIdentifierMatcher matcher, PersistableBundle config) {
+      this.matcher = matcher;
+      this.config = config;
+    }
+
+    /**
+     * A more specific matcher should return a negative value to have higher priority over generic
+     * matchers.
+     */
+    @Override
+    public int compareTo(@NonNull ConfigEntry other) {
+      ComparisonChain comparisonChain = ComparisonChain.start();
+      if (!(matcher.gid1().isPresent() && other.matcher.gid1().isPresent())) {
+        if (matcher.gid1().isPresent()) {
+          return -1;
+        } else if (other.matcher.gid1().isPresent()) {
+          return 1;
+        } else {
+          return 0;
+        }
+      } else {
+        comparisonChain = comparisonChain.compare(matcher.gid1().get(), other.matcher.gid1().get());
+      }
+
+      return comparisonChain.compare(matcher.mccMnc(), other.matcher.mccMnc()).result();
+    }
+  }
+
+  private static final String TAG_PERSISTABLEMAP = "pbundle_as_map";
+
+  /**
+   * A string array of MCCMNC the config applies to. Addtional filters should be appended as the URI
+   * query parameter format.
+   *
+   * <p>For example{@code <string-array name="mccmnc"> <item value="12345?gid1=foo"/> <item
+   * value="67890"/> </string-array> }
+   *
+   * @see #KEY_GID1
+   */
+  @VisibleForTesting static final String KEY_MCCMNC = "mccmnc";
+
+  /**
+   * Additional query parameter in {@link #KEY_MCCMNC} to filter by the Group ID level 1.
+   *
+   * @see CarrierIdentifierMatcher#gid1()
+   */
+  private static final String KEY_GID1 = "gid1";
+
+  private static final String KEY_FEATURE_FLAG_NAME = "feature_flag_name";
+
+  private static Map<String, SortedSet<ConfigEntry>> cachedConfigs;
+
+  private final Map<String, SortedSet<ConfigEntry>> configs;
+
+  public DialerVvmConfigManager(Context context) {
+    if (cachedConfigs == null) {
+      cachedConfigs = loadConfigs(context, context.getResources().getXml(R.xml.vvm_config));
+    }
+    configs = cachedConfigs;
+  }
+
+  @VisibleForTesting
+  DialerVvmConfigManager(Context context, XmlPullParser parser) {
+    configs = loadConfigs(context, parser);
+  }
+
+  @Nullable
+  public PersistableBundle getConfig(CarrierIdentifier carrierIdentifier) {
+    if (!configs.containsKey(carrierIdentifier.mccMnc())) {
+      return null;
+    }
+    for (ConfigEntry configEntry : configs.get(carrierIdentifier.mccMnc())) {
+      if (configEntry.matcher.matches(carrierIdentifier)) {
+        return configEntry.config;
+      }
+    }
+    return null;
+  }
+
+  private static Map<String, SortedSet<ConfigEntry>> loadConfigs(
+      Context context, XmlPullParser parser) {
+    Map<String, SortedSet<ConfigEntry>> configs = new ArrayMap<>();
+    try {
+      ArrayList list = readBundleList(parser);
+      for (Object object : list) {
+        if (!(object instanceof PersistableBundle)) {
+          throw new IllegalArgumentException("PersistableBundle expected, got " + object);
+        }
+        PersistableBundle bundle = (PersistableBundle) object;
+
+        if (bundle.containsKey(KEY_FEATURE_FLAG_NAME)
+            && !ConfigProviderBindings.get(context)
+                .getBoolean(bundle.getString(KEY_FEATURE_FLAG_NAME), false)) {
+          continue;
+        }
+
+        String[] identifiers = bundle.getStringArray(KEY_MCCMNC);
+        if (identifiers == null) {
+          throw new IllegalArgumentException("MCCMNC is null");
+        }
+        for (String identifier : identifiers) {
+          Uri uri = Uri.parse(identifier);
+          String mccMnc = uri.getPath();
+          SortedSet<ConfigEntry> set;
+          if (configs.containsKey(mccMnc)) {
+            set = configs.get(mccMnc);
+          } else {
+            // Need a SortedSet so matchers will be sorted by priority.
+            set = new TreeSet<>();
+            configs.put(mccMnc, set);
+          }
+          CarrierIdentifierMatcher.Builder matcherBuilder = CarrierIdentifierMatcher.builder();
+          matcherBuilder.setMccMnc(mccMnc);
+          if (uri.getQueryParameterNames().contains(KEY_GID1)) {
+            matcherBuilder.setGid1(uri.getQueryParameter(KEY_GID1));
+          }
+          set.add(new ConfigEntry(matcherBuilder.build(), bundle));
+        }
+      }
+    } catch (IOException | XmlPullParserException e) {
+      throw new RuntimeException(e);
+    }
+    return configs;
+  }
+
+  @Nullable
+  public static ArrayList readBundleList(XmlPullParser in)
+      throws IOException, XmlPullParserException {
+    final int outerDepth = in.getDepth();
+    int event;
+    while (((event = in.next()) != XmlPullParser.END_DOCUMENT)
+        && (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
+      if (event == XmlPullParser.START_TAG) {
+        final String startTag = in.getName();
+        final String[] tagName = new String[1];
+        in.next();
+        return XmlUtils.readThisListXml(in, startTag, tagName, new MyReadMapCallback(), false);
+      }
+    }
+    return null;
+  }
+
+  public static PersistableBundle restoreFromXml(XmlPullParser in)
+      throws IOException, XmlPullParserException {
+    final int outerDepth = in.getDepth();
+    final String startTag = in.getName();
+    final String[] tagName = new String[1];
+    int event;
+    while (((event = in.next()) != XmlPullParser.END_DOCUMENT)
+        && (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
+      if (event == XmlPullParser.START_TAG) {
+        ArrayMap<String, ?> map =
+            XmlUtils.readThisArrayMapXml(in, startTag, tagName, new MyReadMapCallback());
+        PersistableBundle result = new PersistableBundle();
+        for (Entry<String, ?> entry : map.entrySet()) {
+          Object value = entry.getValue();
+          if (value instanceof Integer) {
+            result.putInt(entry.getKey(), (int) value);
+          } else if (value instanceof Boolean) {
+            result.putBoolean(entry.getKey(), (boolean) value);
+          } else if (value instanceof String) {
+            result.putString(entry.getKey(), (String) value);
+          } else if (value instanceof String[]) {
+            result.putStringArray(entry.getKey(), (String[]) value);
+          } else if (value instanceof PersistableBundle) {
+            result.putPersistableBundle(entry.getKey(), (PersistableBundle) value);
+          }
+        }
+        return result;
+      }
+    }
+    return PersistableBundle.EMPTY;
+  }
+
+  static class MyReadMapCallback implements XmlUtils.ReadMapCallback {
+
+    @Override
+    public Object readThisUnknownObjectXml(XmlPullParser in, String tag)
+        throws XmlPullParserException, IOException {
+      if (TAG_PERSISTABLEMAP.equals(tag)) {
+        return restoreFromXml(in);
+      }
+      throw new XmlPullParserException("Unknown tag=" + tag);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
index ef62d2a..f8a9e4b 100644
--- a/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
+++ b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
@@ -55,6 +55,7 @@
  * <p>TODO(twyen): refactor this to an interface.
  */
 @TargetApi(VERSION_CODES.O)
+@SuppressWarnings("missingpermission")
 public class OmtpVvmCarrierConfigHelper {
 
   private static final String TAG = "OmtpVvmCarrierCfgHlpr";
@@ -131,7 +132,8 @@
 
       carrierConfig = getCarrierConfig(telephonyManager);
       telephonyConfig =
-          new TelephonyVvmConfigManager(context).getConfig(telephonyManager.getSimOperator());
+          new DialerVvmConfigManager(context)
+              .getConfig(CarrierIdentifier.forHandle(context, phoneAccountHandle));
     }
 
     vvmType = getVvmType();
@@ -199,12 +201,6 @@
   }
 
   @Nullable
-  public Set<String> getCarrierVvmPackageNames() {
-    Assert.checkArgument(isValid());
-    return getCarrierVvmPackageNamesWithoutValidation();
-  }
-
-  @Nullable
   private Set<String> getCarrierVvmPackageNamesWithoutValidation() {
     Set<String> names = getCarrierVvmPackageNames(overrideConfig);
     if (names != null) {
@@ -217,6 +213,12 @@
     return getCarrierVvmPackageNames(telephonyConfig);
   }
 
+  @Nullable
+  public Set<String> getCarrierVvmPackageNames() {
+    Assert.checkArgument(isValid());
+    return getCarrierVvmPackageNamesWithoutValidation();
+  }
+
   private static Set<String> getCarrierVvmPackageNames(@Nullable PersistableBundle bundle) {
     if (bundle == null) {
       return null;
diff --git a/java/com/android/voicemail/impl/TelephonyVvmConfigManager.java b/java/com/android/voicemail/impl/TelephonyVvmConfigManager.java
deleted file mode 100644
index ecf4e6f..0000000
--- a/java/com/android/voicemail/impl/TelephonyVvmConfigManager.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemail.impl;
-
-import android.content.Context;
-import android.os.PersistableBundle;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.util.ArrayMap;
-import com.android.dialer.configprovider.ConfigProviderBindings;
-import com.android.voicemail.impl.utils.XmlUtils;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Map;
-import java.util.Map.Entry;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-/** Load and caches telephony vvm config from res/xml/vvm_config.xml */
-public class TelephonyVvmConfigManager {
-
-  private static final String TAG = "TelephonyVvmCfgMgr";
-
-  private static final boolean USE_DEBUG_CONFIG = false;
-
-  private static final String TAG_PERSISTABLEMAP = "pbundle_as_map";
-
-  @VisibleForTesting static final String KEY_MCCMNC = "mccmnc";
-
-  private static final String KEY_FEATURE_FLAG_NAME = "feature_flag_name";
-
-  private static Map<String, PersistableBundle> cachedConfigs;
-
-  private final Map<String, PersistableBundle> configs;
-
-  public TelephonyVvmConfigManager(Context context) {
-    if (cachedConfigs == null) {
-      cachedConfigs = loadConfigs(context, context.getResources().getXml(R.xml.vvm_config));
-    }
-    configs = cachedConfigs;
-  }
-
-  @VisibleForTesting
-  TelephonyVvmConfigManager(Context context, XmlPullParser parser) {
-    configs = loadConfigs(context, parser);
-  }
-
-  @Nullable
-  public PersistableBundle getConfig(String mccMnc) {
-    if (USE_DEBUG_CONFIG) {
-      return configs.get("TEST");
-    }
-    return configs.get(mccMnc);
-  }
-
-  private static Map<String, PersistableBundle> loadConfigs(Context context, XmlPullParser parser) {
-    Map<String, PersistableBundle> configs = new ArrayMap<>();
-    try {
-      ArrayList list = readBundleList(parser);
-      for (Object object : list) {
-        if (!(object instanceof PersistableBundle)) {
-          throw new IllegalArgumentException("PersistableBundle expected, got " + object);
-        }
-        PersistableBundle bundle = (PersistableBundle) object;
-
-        if (bundle.containsKey(KEY_FEATURE_FLAG_NAME)
-            && !ConfigProviderBindings.get(context)
-                .getBoolean(bundle.getString(KEY_FEATURE_FLAG_NAME), false)) {
-          continue;
-        }
-
-        String[] mccMncs = bundle.getStringArray(KEY_MCCMNC);
-        if (mccMncs == null) {
-          throw new IllegalArgumentException("MCCMNC is null");
-        }
-        for (String mccMnc : mccMncs) {
-          configs.put(mccMnc, bundle);
-        }
-      }
-    } catch (IOException | XmlPullParserException e) {
-      throw new RuntimeException(e);
-    }
-    return configs;
-  }
-
-  @Nullable
-  public static ArrayList readBundleList(XmlPullParser in)
-      throws IOException, XmlPullParserException {
-    final int outerDepth = in.getDepth();
-    int event;
-    while (((event = in.next()) != XmlPullParser.END_DOCUMENT)
-        && (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
-      if (event == XmlPullParser.START_TAG) {
-        final String startTag = in.getName();
-        final String[] tagName = new String[1];
-        in.next();
-        return XmlUtils.readThisListXml(in, startTag, tagName, new MyReadMapCallback(), false);
-      }
-    }
-    return null;
-  }
-
-  public static PersistableBundle restoreFromXml(XmlPullParser in)
-      throws IOException, XmlPullParserException {
-    final int outerDepth = in.getDepth();
-    final String startTag = in.getName();
-    final String[] tagName = new String[1];
-    int event;
-    while (((event = in.next()) != XmlPullParser.END_DOCUMENT)
-        && (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
-      if (event == XmlPullParser.START_TAG) {
-        ArrayMap<String, ?> map =
-            XmlUtils.readThisArrayMapXml(in, startTag, tagName, new MyReadMapCallback());
-        PersistableBundle result = new PersistableBundle();
-        for (Entry<String, ?> entry : map.entrySet()) {
-          Object value = entry.getValue();
-          if (value instanceof Integer) {
-            result.putInt(entry.getKey(), (int) value);
-          } else if (value instanceof Boolean) {
-            result.putBoolean(entry.getKey(), (boolean) value);
-          } else if (value instanceof String) {
-            result.putString(entry.getKey(), (String) value);
-          } else if (value instanceof String[]) {
-            result.putStringArray(entry.getKey(), (String[]) value);
-          } else if (value instanceof PersistableBundle) {
-            result.putPersistableBundle(entry.getKey(), (PersistableBundle) value);
-          }
-        }
-        return result;
-      }
-    }
-    return PersistableBundle.EMPTY;
-  }
-
-  static class MyReadMapCallback implements XmlUtils.ReadMapCallback {
-
-    @Override
-    public Object readThisUnknownObjectXml(XmlPullParser in, String tag)
-        throws XmlPullParserException, IOException {
-      if (TAG_PERSISTABLEMAP.equals(tag)) {
-        return restoreFromXml(in);
-      }
-      throw new XmlPullParserException("Unknown tag=" + tag);
-    }
-  }
-}