1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.voicemail.impl;
18 
19 import android.content.Context;
20 import android.net.Uri;
21 import android.os.PersistableBundle;
22 import android.support.annotation.NonNull;
23 import android.support.annotation.Nullable;
24 import android.support.annotation.VisibleForTesting;
25 import android.util.ArrayMap;
26 import com.android.dialer.configprovider.ConfigProviderComponent;
27 import com.android.voicemail.impl.utils.XmlUtils;
28 import com.google.common.collect.ComparisonChain;
29 import java.io.IOException;
30 import java.util.ArrayList;
31 import java.util.Map;
32 import java.util.Map.Entry;
33 import java.util.SortedSet;
34 import java.util.TreeSet;
35 import org.xmlpull.v1.XmlPullParser;
36 import org.xmlpull.v1.XmlPullParserException;
37 
38 /** Load and caches dialer vvm config from res/xml/vvm_config.xml */
39 public class DialerVvmConfigManager {
40   private static class ConfigEntry implements Comparable<ConfigEntry> {
41 
42     final CarrierIdentifierMatcher matcher;
43     final PersistableBundle config;
44 
ConfigEntry(CarrierIdentifierMatcher matcher, PersistableBundle config)45     ConfigEntry(CarrierIdentifierMatcher matcher, PersistableBundle config) {
46       this.matcher = matcher;
47       this.config = config;
48     }
49 
50     /**
51      * A more specific matcher should return a negative value to have higher priority over generic
52      * matchers.
53      */
54     @Override
compareTo(@onNull ConfigEntry other)55     public int compareTo(@NonNull ConfigEntry other) {
56       ComparisonChain comparisonChain = ComparisonChain.start();
57       if (!(matcher.gid1().isPresent() && other.matcher.gid1().isPresent())) {
58         if (matcher.gid1().isPresent()) {
59           return -1;
60         } else if (other.matcher.gid1().isPresent()) {
61           return 1;
62         } else {
63           return 0;
64         }
65       } else {
66         comparisonChain = comparisonChain.compare(matcher.gid1().get(), other.matcher.gid1().get());
67       }
68 
69       return comparisonChain.compare(matcher.mccMnc(), other.matcher.mccMnc()).result();
70     }
71   }
72 
73   private static final String TAG_PERSISTABLEMAP = "pbundle_as_map";
74 
75   /**
76    * A string array of MCCMNC the config applies to. Addtional filters should be appended as the URI
77    * query parameter format.
78    *
79    * <p>For example{@code <string-array name="mccmnc"> <item value="12345?gid1=foo"/> <item
80    * value="67890"/> </string-array> }
81    *
82    * @see #KEY_GID1
83    */
84   @VisibleForTesting static final String KEY_MCCMNC = "mccmnc";
85 
86   /**
87    * Additional query parameter in {@link #KEY_MCCMNC} to filter by the Group ID level 1.
88    *
89    * @see CarrierIdentifierMatcher#gid1()
90    */
91   private static final String KEY_GID1 = "gid1";
92 
93   private static final String KEY_FEATURE_FLAG_NAME = "feature_flag_name";
94 
95   private static Map<String, SortedSet<ConfigEntry>> cachedConfigs;
96 
97   private final Map<String, SortedSet<ConfigEntry>> configs;
98 
DialerVvmConfigManager(Context context)99   public DialerVvmConfigManager(Context context) {
100     if (cachedConfigs == null) {
101       cachedConfigs = loadConfigs(context, context.getResources().getXml(R.xml.vvm_config));
102     }
103     configs = cachedConfigs;
104   }
105 
106   @VisibleForTesting
DialerVvmConfigManager(Context context, XmlPullParser parser)107   DialerVvmConfigManager(Context context, XmlPullParser parser) {
108     configs = loadConfigs(context, parser);
109   }
110 
111   @Nullable
getConfig(CarrierIdentifier carrierIdentifier)112   public PersistableBundle getConfig(CarrierIdentifier carrierIdentifier) {
113     if (!configs.containsKey(carrierIdentifier.mccMnc())) {
114       return null;
115     }
116     for (ConfigEntry configEntry : configs.get(carrierIdentifier.mccMnc())) {
117       if (configEntry.matcher.matches(carrierIdentifier)) {
118         return configEntry.config;
119       }
120     }
121     return null;
122   }
123 
loadConfigs( Context context, XmlPullParser parser)124   private static Map<String, SortedSet<ConfigEntry>> loadConfigs(
125       Context context, XmlPullParser parser) {
126     Map<String, SortedSet<ConfigEntry>> configs = new ArrayMap<>();
127     try {
128       ArrayList list = readBundleList(parser);
129       for (Object object : list) {
130         if (!(object instanceof PersistableBundle)) {
131           throw new IllegalArgumentException("PersistableBundle expected, got " + object);
132         }
133         PersistableBundle bundle = (PersistableBundle) object;
134 
135         if (bundle.containsKey(KEY_FEATURE_FLAG_NAME)
136             && !ConfigProviderComponent.get(context)
137                 .getConfigProvider()
138                 .getBoolean(bundle.getString(KEY_FEATURE_FLAG_NAME), false)) {
139           continue;
140         }
141 
142         String[] identifiers = bundle.getStringArray(KEY_MCCMNC);
143         if (identifiers == null) {
144           throw new IllegalArgumentException("MCCMNC is null");
145         }
146         for (String identifier : identifiers) {
147           Uri uri = Uri.parse(identifier);
148           String mccMnc = uri.getPath();
149           SortedSet<ConfigEntry> set;
150           if (configs.containsKey(mccMnc)) {
151             set = configs.get(mccMnc);
152           } else {
153             // Need a SortedSet so matchers will be sorted by priority.
154             set = new TreeSet<>();
155             configs.put(mccMnc, set);
156           }
157           CarrierIdentifierMatcher.Builder matcherBuilder = CarrierIdentifierMatcher.builder();
158           matcherBuilder.setMccMnc(mccMnc);
159           if (uri.getQueryParameterNames().contains(KEY_GID1)) {
160             matcherBuilder.setGid1(uri.getQueryParameter(KEY_GID1));
161           }
162           set.add(new ConfigEntry(matcherBuilder.build(), bundle));
163         }
164       }
165     } catch (IOException | XmlPullParserException e) {
166       throw new RuntimeException(e);
167     }
168     return configs;
169   }
170 
171   @Nullable
readBundleList(XmlPullParser in)172   public static ArrayList readBundleList(XmlPullParser in)
173       throws IOException, XmlPullParserException {
174     final int outerDepth = in.getDepth();
175     int event;
176     while (((event = in.next()) != XmlPullParser.END_DOCUMENT)
177         && (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
178       if (event == XmlPullParser.START_TAG) {
179         final String startTag = in.getName();
180         final String[] tagName = new String[1];
181         in.next();
182         return XmlUtils.readThisListXml(in, startTag, tagName, new MyReadMapCallback(), false);
183       }
184     }
185     return null;
186   }
187 
restoreFromXml(XmlPullParser in)188   public static PersistableBundle restoreFromXml(XmlPullParser in)
189       throws IOException, XmlPullParserException {
190     final int outerDepth = in.getDepth();
191     final String startTag = in.getName();
192     final String[] tagName = new String[1];
193     int event;
194     while (((event = in.next()) != XmlPullParser.END_DOCUMENT)
195         && (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
196       if (event == XmlPullParser.START_TAG) {
197         ArrayMap<String, ?> map =
198             XmlUtils.readThisArrayMapXml(in, startTag, tagName, new MyReadMapCallback());
199         PersistableBundle result = new PersistableBundle();
200         for (Entry<String, ?> entry : map.entrySet()) {
201           Object value = entry.getValue();
202           if (value instanceof Integer) {
203             result.putInt(entry.getKey(), (int) value);
204           } else if (value instanceof Boolean) {
205             result.putBoolean(entry.getKey(), (boolean) value);
206           } else if (value instanceof String) {
207             result.putString(entry.getKey(), (String) value);
208           } else if (value instanceof String[]) {
209             result.putStringArray(entry.getKey(), (String[]) value);
210           } else if (value instanceof PersistableBundle) {
211             result.putPersistableBundle(entry.getKey(), (PersistableBundle) value);
212           }
213         }
214         return result;
215       }
216     }
217     return PersistableBundle.EMPTY;
218   }
219 
220   static class MyReadMapCallback implements XmlUtils.ReadMapCallback {
221 
222     @Override
readThisUnknownObjectXml(XmlPullParser in, String tag)223     public Object readThisUnknownObjectXml(XmlPullParser in, String tag)
224         throws XmlPullParserException, IOException {
225       if (TAG_PERSISTABLEMAP.equals(tag)) {
226         return restoreFromXml(in);
227       }
228       throw new XmlPullParserException("Unknown tag=" + tag);
229     }
230   }
231 }
232