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