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