1 /* 2 * Copyright (C) 2020 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 android.telephony.ims; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.os.Build; 25 import android.provider.Telephony.SimInfo; 26 import android.text.TextUtils; 27 import android.util.ArrayMap; 28 import android.util.ArraySet; 29 30 import com.android.telephony.Rlog; 31 32 import org.xmlpull.v1.XmlPullParser; 33 import org.xmlpull.v1.XmlPullParserException; 34 import org.xmlpull.v1.XmlPullParserFactory; 35 36 import java.io.ByteArrayInputStream; 37 import java.io.ByteArrayOutputStream; 38 import java.io.IOException; 39 import java.util.Locale; 40 import java.util.Map; 41 import java.util.Objects; 42 import java.util.Set; 43 import java.util.zip.GZIPInputStream; 44 import java.util.zip.GZIPOutputStream; 45 46 /** 47 * RCS config data and methods to process the config 48 * @hide 49 */ 50 public final class RcsConfig { 51 private static final String LOG_TAG = "RcsConfig"; 52 private static final boolean DBG = Build.IS_ENG; 53 54 // Tag and attribute defined in RCC.07 A.2 55 private static final String TAG_CHARACTERISTIC = "characteristic"; 56 private static final String TAG_PARM = "parm"; 57 private static final String ATTRIBUTE_TYPE = "type"; 58 private static final String ATTRIBUTE_NAME = "name"; 59 private static final String ATTRIBUTE_VALUE = "value"; 60 // Keyword for Rcs Volte single registration defined in RCC.07 A.1.6.2 61 private static final String PARM_SINGLE_REGISTRATION = "rcsVolteSingleRegistration"; 62 63 /** 64 * Characteristic of the RCS provisioning config 65 */ 66 public static class Characteristic { 67 private String mType; 68 private final Map<String, String> mParms = new ArrayMap<>(); 69 private final Set<Characteristic> mSubs = new ArraySet<>(); 70 private final Characteristic mParent; 71 Characteristic(String type, Characteristic parent)72 private Characteristic(String type, Characteristic parent) { 73 mType = type; 74 mParent = parent; 75 } 76 getType()77 private String getType() { 78 return mType; 79 } 80 getParms()81 private Map<String, String> getParms() { 82 return mParms; 83 } 84 getSubs()85 private Set<Characteristic> getSubs() { 86 return mSubs; 87 } 88 getParent()89 private Characteristic getParent() { 90 return mParent; 91 } 92 getSubByType(String type)93 private Characteristic getSubByType(String type) { 94 if (TextUtils.equals(mType, type)) { 95 return this; 96 } 97 Characteristic result = null; 98 for (Characteristic sub : mSubs) { 99 result = sub.getSubByType(type); 100 if (result != null) { 101 break; 102 } 103 } 104 return result; 105 } 106 hasSubByType(String type)107 private boolean hasSubByType(String type) { 108 return getSubByType(type) != null; 109 } 110 getParmValue(String name)111 private String getParmValue(String name) { 112 String value = mParms.get(name); 113 if (value == null) { 114 for (Characteristic sub : mSubs) { 115 value = sub.getParmValue(name); 116 if (value != null) { 117 break; 118 } 119 } 120 } 121 return value; 122 } 123 hasParm(String name)124 boolean hasParm(String name) { 125 if (mParms.containsKey(name)) { 126 return true; 127 } 128 129 for (Characteristic sub : mSubs) { 130 if (sub.hasParm(name)) { 131 return true; 132 } 133 } 134 135 return false; 136 } 137 138 @Override toString()139 public String toString() { 140 final StringBuilder sb = new StringBuilder(); 141 sb.append("[" + mType + "]: "); 142 if (DBG) { 143 sb.append(mParms); 144 } 145 for (Characteristic sub : mSubs) { 146 sb.append("\n"); 147 sb.append(sub.toString().replace("\n", "\n\t")); 148 } 149 return sb.toString(); 150 } 151 152 @Override equals(Object obj)153 public boolean equals(Object obj) { 154 if (!(obj instanceof Characteristic)) { 155 return false; 156 } 157 158 Characteristic o = (Characteristic) obj; 159 160 return TextUtils.equals(mType, o.mType) && mParms.equals(o.mParms) 161 && mSubs.equals(o.mSubs); 162 } 163 164 @Override hashCode()165 public int hashCode() { 166 return Objects.hash(mType, mParms, mSubs); 167 } 168 } 169 170 private final Characteristic mRoot; 171 private Characteristic mCurrent; 172 private final byte[] mData; 173 RcsConfig(byte[] data)174 public RcsConfig(byte[] data) throws IllegalArgumentException { 175 if (data == null || data.length == 0) { 176 throw new IllegalArgumentException("Empty data"); 177 } 178 mRoot = new Characteristic(null, null); 179 mCurrent = mRoot; 180 mData = data; 181 Characteristic current = mRoot; 182 ByteArrayInputStream inputStream = new ByteArrayInputStream(data); 183 try { 184 XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 185 factory.setNamespaceAware(true); 186 XmlPullParser xpp = factory.newPullParser(); 187 xpp.setInput(inputStream, null); 188 int eventType = xpp.getEventType(); 189 String tag = null; 190 while (eventType != XmlPullParser.END_DOCUMENT && current != null) { 191 if (eventType == XmlPullParser.START_TAG) { 192 tag = xpp.getName().trim().toLowerCase(Locale.ROOT); 193 if (TAG_CHARACTERISTIC.equals(tag)) { 194 int count = xpp.getAttributeCount(); 195 String type = null; 196 if (count > 0) { 197 for (int i = 0; i < count; i++) { 198 String name = xpp.getAttributeName(i).trim() 199 .toLowerCase(Locale.ROOT); 200 if (ATTRIBUTE_TYPE.equals(name)) { 201 type = xpp.getAttributeValue(xpp.getAttributeNamespace(i), 202 name).trim().toLowerCase(Locale.ROOT); 203 break; 204 } 205 } 206 } 207 Characteristic next = new Characteristic(type, current); 208 current.getSubs().add(next); 209 current = next; 210 } else if (TAG_PARM.equals(tag)) { 211 int count = xpp.getAttributeCount(); 212 String key = null; 213 String value = null; 214 if (count > 1) { 215 for (int i = 0; i < count; i++) { 216 String name = xpp.getAttributeName(i).trim() 217 .toLowerCase(Locale.ROOT); 218 if (ATTRIBUTE_NAME.equals(name)) { 219 key = xpp.getAttributeValue(xpp.getAttributeNamespace(i), 220 name).trim().toLowerCase(Locale.ROOT); 221 } else if (ATTRIBUTE_VALUE.equals(name)) { 222 value = xpp.getAttributeValue(xpp.getAttributeNamespace(i), 223 name).trim(); 224 } 225 } 226 } 227 if (key != null && value != null) { 228 current.getParms().put(key, value); 229 } 230 } 231 } else if (eventType == XmlPullParser.END_TAG) { 232 tag = xpp.getName().trim().toLowerCase(Locale.ROOT); 233 if (TAG_CHARACTERISTIC.equals(tag)) { 234 current = current.getParent(); 235 } 236 tag = null; 237 } 238 eventType = xpp.next(); 239 } 240 } catch (IOException | XmlPullParserException e) { 241 throw new IllegalArgumentException(e); 242 } finally { 243 try { 244 inputStream.close(); 245 } catch (IOException e) { 246 loge("error to close input stream, skip."); 247 } 248 } 249 } 250 251 /** 252 * Retrieve a String value of the config item with the tag 253 * 254 * @param tag The name of the config to retrieve. 255 * @param defaultVal Value to return if the config does not exist. 256 * 257 * @return Returns the config value if it exists, or defaultVal. 258 */ getString(@onNull String tag, @Nullable String defaultVal)259 public @Nullable String getString(@NonNull String tag, @Nullable String defaultVal) { 260 String value = mCurrent.getParmValue(tag.trim().toLowerCase(Locale.ROOT)); 261 return value != null ? value : defaultVal; 262 } 263 264 /** 265 * Retrieve a int value of the config item with the tag 266 * 267 * @param tag The name of the config to retrieve. 268 * @param defaultVal Value to return if the config does not exist or not valid. 269 * 270 * @return Returns the config value if it exists and is a valid int, or defaultVal. 271 */ getInteger(@onNull String tag, int defaultVal)272 public int getInteger(@NonNull String tag, int defaultVal) { 273 try { 274 return Integer.parseInt(getString(tag, null)); 275 } catch (NumberFormatException e) { 276 logd("error to getInteger for " + tag + " due to " + e); 277 } 278 return defaultVal; 279 } 280 281 /** 282 * Retrieve a boolean value of the config item with the tag 283 * 284 * @param tag The name of the config to retrieve. 285 * @param defaultVal Value to return if the config does not exist. 286 * 287 * @return Returns the config value if it exists, or defaultVal. 288 */ getBoolean(@onNull String tag, boolean defaultVal)289 public boolean getBoolean(@NonNull String tag, boolean defaultVal) { 290 String value = getString(tag, null); 291 return value != null ? Boolean.parseBoolean(value) : defaultVal; 292 } 293 294 /** 295 * Check whether the config item exists 296 * 297 * @param tag The name of the config to retrieve. 298 * 299 * @return Returns true if it exists, or false. 300 */ hasConfig(@onNull String tag)301 public boolean hasConfig(@NonNull String tag) { 302 return mCurrent.hasParm(tag.trim().toLowerCase(Locale.ROOT)); 303 } 304 305 /** 306 * Return the Characteristic with the given type 307 */ getCharacteristic(@onNull String type)308 public @Nullable Characteristic getCharacteristic(@NonNull String type) { 309 return mCurrent.getSubByType(type.trim().toLowerCase(Locale.ROOT)); 310 } 311 312 /** 313 * Check whether the Characteristic with the given type exists 314 */ hasCharacteristic(@onNull String type)315 public boolean hasCharacteristic(@NonNull String type) { 316 return mCurrent.getSubByType(type.trim().toLowerCase(Locale.ROOT)) != null; 317 } 318 319 /** 320 * Set current Characteristic to given Characteristic 321 */ setCurrentCharacteristic(@onNull Characteristic current)322 public void setCurrentCharacteristic(@NonNull Characteristic current) { 323 if (current != null) { 324 mCurrent = current; 325 } 326 } 327 328 /** 329 * Move current Characteristic to parent layer 330 */ moveToParent()331 public boolean moveToParent() { 332 if (mCurrent.getParent() == null) { 333 return false; 334 } 335 mCurrent = mCurrent.getParent(); 336 return true; 337 } 338 339 /** 340 * Move current Characteristic to the root 341 */ moveToRoot()342 public void moveToRoot() { 343 mCurrent = mRoot; 344 } 345 346 /** 347 * Return root Characteristic 348 */ getRoot()349 public @NonNull Characteristic getRoot() { 350 return mRoot; 351 } 352 353 /** 354 * Return current Characteristic 355 */ getCurrentCharacteristic()356 public @NonNull Characteristic getCurrentCharacteristic() { 357 return mCurrent; 358 } 359 360 /** 361 * Check whether Rcs Volte single registration is supported by the config. 362 */ isRcsVolteSingleRegistrationSupported(boolean isRoaming)363 public boolean isRcsVolteSingleRegistrationSupported(boolean isRoaming) { 364 int val = getInteger(PARM_SINGLE_REGISTRATION, 1); 365 return isRoaming ? val == 1 : val > 0; 366 } 367 368 @Override toString()369 public String toString() { 370 final StringBuilder sb = new StringBuilder(); 371 sb.append("[RCS Config]"); 372 if (DBG) { 373 sb.append("=== Root ===\n"); 374 sb.append(mRoot); 375 sb.append("=== Current ===\n"); 376 sb.append(mCurrent); 377 } 378 return sb.toString(); 379 } 380 381 @Override equals(Object obj)382 public boolean equals(Object obj) { 383 if (!(obj instanceof RcsConfig)) { 384 return false; 385 } 386 387 RcsConfig other = (RcsConfig) obj; 388 389 return mRoot.equals(other.mRoot) && mCurrent.equals(other.mCurrent); 390 } 391 392 @Override hashCode()393 public int hashCode() { 394 return Objects.hash(mRoot, mCurrent); 395 } 396 397 /** 398 * compress the gzip format data 399 */ compressGzip(@onNull byte[] data)400 public static @Nullable byte[] compressGzip(@NonNull byte[] data) { 401 if (data == null || data.length == 0) { 402 return data; 403 } 404 byte[] out = null; 405 try { 406 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length); 407 GZIPOutputStream gzipCompressingStream = 408 new GZIPOutputStream(outputStream); 409 gzipCompressingStream.write(data); 410 gzipCompressingStream.close(); 411 out = outputStream.toByteArray(); 412 outputStream.close(); 413 } catch (IOException e) { 414 loge("Error to compressGzip due to " + e); 415 } 416 return out; 417 } 418 419 /** 420 * decompress the gzip format data 421 */ decompressGzip(@onNull byte[] data)422 public static @Nullable byte[] decompressGzip(@NonNull byte[] data) { 423 if (data == null || data.length == 0) { 424 return data; 425 } 426 byte[] out = null; 427 try { 428 ByteArrayInputStream inputStream = new ByteArrayInputStream(data); 429 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 430 GZIPInputStream gzipDecompressingStream = 431 new GZIPInputStream(inputStream); 432 byte[] buf = new byte[1024]; 433 int size = gzipDecompressingStream.read(buf); 434 while (size >= 0) { 435 outputStream.write(buf, 0, size); 436 size = gzipDecompressingStream.read(buf); 437 } 438 gzipDecompressingStream.close(); 439 inputStream.close(); 440 out = outputStream.toByteArray(); 441 outputStream.close(); 442 } catch (IOException e) { 443 loge("Error to decompressGzip due to " + e); 444 } 445 return out; 446 } 447 448 /** 449 * save the config to siminfo db. It is only used internally. 450 */ updateConfigForSub(@onNull Context cxt, int subId, @NonNull byte[] config, boolean isCompressed)451 public static void updateConfigForSub(@NonNull Context cxt, int subId, 452 @NonNull byte[] config, boolean isCompressed) { 453 //always store gzip compressed data 454 byte[] data = isCompressed ? config : compressGzip(config); 455 ContentValues values = new ContentValues(); 456 values.put(SimInfo.COLUMN_RCS_CONFIG, data); 457 cxt.getContentResolver().update(SimInfo.CONTENT_URI, values, 458 SimInfo.COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID + "=" + subId, null); 459 } 460 461 /** 462 * load the config from siminfo db. It is only used internally. 463 */ loadRcsConfigForSub(@onNull Context cxt, int subId, boolean isCompressed)464 public static @Nullable byte[] loadRcsConfigForSub(@NonNull Context cxt, 465 int subId, boolean isCompressed) { 466 467 byte[] data = null; 468 469 Cursor cursor = cxt.getContentResolver().query(SimInfo.CONTENT_URI, null, 470 SimInfo.COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID + "=" + subId, null, null); 471 try { 472 if (cursor != null && cursor.moveToFirst()) { 473 data = cursor.getBlob(cursor.getColumnIndexOrThrow(SimInfo.COLUMN_RCS_CONFIG)); 474 } 475 } catch (Exception e) { 476 loge("error to load rcs config for sub:" + subId + " due to " + e); 477 } finally { 478 if (cursor != null) { 479 cursor.close(); 480 } 481 } 482 return isCompressed ? data : decompressGzip(data); 483 } 484 logd(String msg)485 private static void logd(String msg) { 486 Rlog.d(LOG_TAG, msg); 487 } 488 loge(String msg)489 private static void loge(String msg) { 490 Rlog.e(LOG_TAG, msg); 491 } 492 } 493