493 lines
16 KiB
Java
493 lines
16 KiB
Java
/*
|
|
* Copyright (C) 2020 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 android.telephony.ims;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.database.Cursor;
|
|
import android.os.Build;
|
|
import android.provider.Telephony.SimInfo;
|
|
import android.text.TextUtils;
|
|
import android.util.ArrayMap;
|
|
import android.util.ArraySet;
|
|
|
|
import com.android.telephony.Rlog;
|
|
|
|
import org.xmlpull.v1.XmlPullParser;
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
import org.xmlpull.v1.XmlPullParserFactory;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.zip.GZIPInputStream;
|
|
import java.util.zip.GZIPOutputStream;
|
|
|
|
/**
|
|
* RCS config data and methods to process the config
|
|
* @hide
|
|
*/
|
|
public final class RcsConfig {
|
|
private static final String LOG_TAG = "RcsConfig";
|
|
private static final boolean DBG = Build.IS_ENG;
|
|
|
|
// Tag and attribute defined in RCC.07 A.2
|
|
private static final String TAG_CHARACTERISTIC = "characteristic";
|
|
private static final String TAG_PARM = "parm";
|
|
private static final String ATTRIBUTE_TYPE = "type";
|
|
private static final String ATTRIBUTE_NAME = "name";
|
|
private static final String ATTRIBUTE_VALUE = "value";
|
|
// Keyword for Rcs Volte single registration defined in RCC.07 A.1.6.2
|
|
private static final String PARM_SINGLE_REGISTRATION = "rcsVolteSingleRegistration";
|
|
|
|
/**
|
|
* Characteristic of the RCS provisioning config
|
|
*/
|
|
public static class Characteristic {
|
|
private String mType;
|
|
private final Map<String, String> mParms = new ArrayMap<>();
|
|
private final Set<Characteristic> mSubs = new ArraySet<>();
|
|
private final Characteristic mParent;
|
|
|
|
private Characteristic(String type, Characteristic parent) {
|
|
mType = type;
|
|
mParent = parent;
|
|
}
|
|
|
|
private String getType() {
|
|
return mType;
|
|
}
|
|
|
|
private Map<String, String> getParms() {
|
|
return mParms;
|
|
}
|
|
|
|
private Set<Characteristic> getSubs() {
|
|
return mSubs;
|
|
}
|
|
|
|
private Characteristic getParent() {
|
|
return mParent;
|
|
}
|
|
|
|
private Characteristic getSubByType(String type) {
|
|
if (TextUtils.equals(mType, type)) {
|
|
return this;
|
|
}
|
|
Characteristic result = null;
|
|
for (Characteristic sub : mSubs) {
|
|
result = sub.getSubByType(type);
|
|
if (result != null) {
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private boolean hasSubByType(String type) {
|
|
return getSubByType(type) != null;
|
|
}
|
|
|
|
private String getParmValue(String name) {
|
|
String value = mParms.get(name);
|
|
if (value == null) {
|
|
for (Characteristic sub : mSubs) {
|
|
value = sub.getParmValue(name);
|
|
if (value != null) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
boolean hasParm(String name) {
|
|
if (mParms.containsKey(name)) {
|
|
return true;
|
|
}
|
|
|
|
for (Characteristic sub : mSubs) {
|
|
if (sub.hasParm(name)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
final StringBuilder sb = new StringBuilder();
|
|
sb.append("[" + mType + "]: ");
|
|
if (DBG) {
|
|
sb.append(mParms);
|
|
}
|
|
for (Characteristic sub : mSubs) {
|
|
sb.append("\n");
|
|
sb.append(sub.toString().replace("\n", "\n\t"));
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
if (!(obj instanceof Characteristic)) {
|
|
return false;
|
|
}
|
|
|
|
Characteristic o = (Characteristic) obj;
|
|
|
|
return TextUtils.equals(mType, o.mType) && mParms.equals(o.mParms)
|
|
&& mSubs.equals(o.mSubs);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(mType, mParms, mSubs);
|
|
}
|
|
}
|
|
|
|
private final Characteristic mRoot;
|
|
private Characteristic mCurrent;
|
|
private final byte[] mData;
|
|
|
|
public RcsConfig(byte[] data) throws IllegalArgumentException {
|
|
if (data == null || data.length == 0) {
|
|
throw new IllegalArgumentException("Empty data");
|
|
}
|
|
mRoot = new Characteristic(null, null);
|
|
mCurrent = mRoot;
|
|
mData = data;
|
|
Characteristic current = mRoot;
|
|
ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
|
|
try {
|
|
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
|
factory.setNamespaceAware(true);
|
|
XmlPullParser xpp = factory.newPullParser();
|
|
xpp.setInput(inputStream, null);
|
|
int eventType = xpp.getEventType();
|
|
String tag = null;
|
|
while (eventType != XmlPullParser.END_DOCUMENT && current != null) {
|
|
if (eventType == XmlPullParser.START_TAG) {
|
|
tag = xpp.getName().trim().toLowerCase(Locale.ROOT);
|
|
if (TAG_CHARACTERISTIC.equals(tag)) {
|
|
int count = xpp.getAttributeCount();
|
|
String type = null;
|
|
if (count > 0) {
|
|
for (int i = 0; i < count; i++) {
|
|
String name = xpp.getAttributeName(i).trim()
|
|
.toLowerCase(Locale.ROOT);
|
|
if (ATTRIBUTE_TYPE.equals(name)) {
|
|
type = xpp.getAttributeValue(xpp.getAttributeNamespace(i),
|
|
name).trim().toLowerCase(Locale.ROOT);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
Characteristic next = new Characteristic(type, current);
|
|
current.getSubs().add(next);
|
|
current = next;
|
|
} else if (TAG_PARM.equals(tag)) {
|
|
int count = xpp.getAttributeCount();
|
|
String key = null;
|
|
String value = null;
|
|
if (count > 1) {
|
|
for (int i = 0; i < count; i++) {
|
|
String name = xpp.getAttributeName(i).trim()
|
|
.toLowerCase(Locale.ROOT);
|
|
if (ATTRIBUTE_NAME.equals(name)) {
|
|
key = xpp.getAttributeValue(xpp.getAttributeNamespace(i),
|
|
name).trim().toLowerCase(Locale.ROOT);
|
|
} else if (ATTRIBUTE_VALUE.equals(name)) {
|
|
value = xpp.getAttributeValue(xpp.getAttributeNamespace(i),
|
|
name).trim();
|
|
}
|
|
}
|
|
}
|
|
if (key != null && value != null) {
|
|
current.getParms().put(key, value);
|
|
}
|
|
}
|
|
} else if (eventType == XmlPullParser.END_TAG) {
|
|
tag = xpp.getName().trim().toLowerCase(Locale.ROOT);
|
|
if (TAG_CHARACTERISTIC.equals(tag)) {
|
|
current = current.getParent();
|
|
}
|
|
tag = null;
|
|
}
|
|
eventType = xpp.next();
|
|
}
|
|
} catch (IOException | XmlPullParserException e) {
|
|
throw new IllegalArgumentException(e);
|
|
} finally {
|
|
try {
|
|
inputStream.close();
|
|
} catch (IOException e) {
|
|
loge("error to close input stream, skip.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve a String value of the config item with the tag
|
|
*
|
|
* @param tag The name of the config to retrieve.
|
|
* @param defaultVal Value to return if the config does not exist.
|
|
*
|
|
* @return Returns the config value if it exists, or defaultVal.
|
|
*/
|
|
public @Nullable String getString(@NonNull String tag, @Nullable String defaultVal) {
|
|
String value = mCurrent.getParmValue(tag.trim().toLowerCase(Locale.ROOT));
|
|
return value != null ? value : defaultVal;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a int value of the config item with the tag
|
|
*
|
|
* @param tag The name of the config to retrieve.
|
|
* @param defaultVal Value to return if the config does not exist or not valid.
|
|
*
|
|
* @return Returns the config value if it exists and is a valid int, or defaultVal.
|
|
*/
|
|
public int getInteger(@NonNull String tag, int defaultVal) {
|
|
try {
|
|
return Integer.parseInt(getString(tag, null));
|
|
} catch (NumberFormatException e) {
|
|
logd("error to getInteger for " + tag + " due to " + e);
|
|
}
|
|
return defaultVal;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a boolean value of the config item with the tag
|
|
*
|
|
* @param tag The name of the config to retrieve.
|
|
* @param defaultVal Value to return if the config does not exist.
|
|
*
|
|
* @return Returns the config value if it exists, or defaultVal.
|
|
*/
|
|
public boolean getBoolean(@NonNull String tag, boolean defaultVal) {
|
|
String value = getString(tag, null);
|
|
return value != null ? Boolean.parseBoolean(value) : defaultVal;
|
|
}
|
|
|
|
/**
|
|
* Check whether the config item exists
|
|
*
|
|
* @param tag The name of the config to retrieve.
|
|
*
|
|
* @return Returns true if it exists, or false.
|
|
*/
|
|
public boolean hasConfig(@NonNull String tag) {
|
|
return mCurrent.hasParm(tag.trim().toLowerCase(Locale.ROOT));
|
|
}
|
|
|
|
/**
|
|
* Return the Characteristic with the given type
|
|
*/
|
|
public @Nullable Characteristic getCharacteristic(@NonNull String type) {
|
|
return mCurrent.getSubByType(type.trim().toLowerCase(Locale.ROOT));
|
|
}
|
|
|
|
/**
|
|
* Check whether the Characteristic with the given type exists
|
|
*/
|
|
public boolean hasCharacteristic(@NonNull String type) {
|
|
return mCurrent.getSubByType(type.trim().toLowerCase(Locale.ROOT)) != null;
|
|
}
|
|
|
|
/**
|
|
* Set current Characteristic to given Characteristic
|
|
*/
|
|
public void setCurrentCharacteristic(@NonNull Characteristic current) {
|
|
if (current != null) {
|
|
mCurrent = current;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move current Characteristic to parent layer
|
|
*/
|
|
public boolean moveToParent() {
|
|
if (mCurrent.getParent() == null) {
|
|
return false;
|
|
}
|
|
mCurrent = mCurrent.getParent();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Move current Characteristic to the root
|
|
*/
|
|
public void moveToRoot() {
|
|
mCurrent = mRoot;
|
|
}
|
|
|
|
/**
|
|
* Return root Characteristic
|
|
*/
|
|
public @NonNull Characteristic getRoot() {
|
|
return mRoot;
|
|
}
|
|
|
|
/**
|
|
* Return current Characteristic
|
|
*/
|
|
public @NonNull Characteristic getCurrentCharacteristic() {
|
|
return mCurrent;
|
|
}
|
|
|
|
/**
|
|
* Check whether Rcs Volte single registration is supported by the config.
|
|
*/
|
|
public boolean isRcsVolteSingleRegistrationSupported(boolean isRoaming) {
|
|
int val = getInteger(PARM_SINGLE_REGISTRATION, 1);
|
|
return isRoaming ? val == 1 : val > 0;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
final StringBuilder sb = new StringBuilder();
|
|
sb.append("[RCS Config]");
|
|
if (DBG) {
|
|
sb.append("=== Root ===\n");
|
|
sb.append(mRoot);
|
|
sb.append("=== Current ===\n");
|
|
sb.append(mCurrent);
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
if (!(obj instanceof RcsConfig)) {
|
|
return false;
|
|
}
|
|
|
|
RcsConfig other = (RcsConfig) obj;
|
|
|
|
return mRoot.equals(other.mRoot) && mCurrent.equals(other.mCurrent);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(mRoot, mCurrent);
|
|
}
|
|
|
|
/**
|
|
* compress the gzip format data
|
|
*/
|
|
public static @Nullable byte[] compressGzip(@NonNull byte[] data) {
|
|
if (data == null || data.length == 0) {
|
|
return data;
|
|
}
|
|
byte[] out = null;
|
|
try {
|
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length);
|
|
GZIPOutputStream gzipCompressingStream =
|
|
new GZIPOutputStream(outputStream);
|
|
gzipCompressingStream.write(data);
|
|
gzipCompressingStream.close();
|
|
out = outputStream.toByteArray();
|
|
outputStream.close();
|
|
} catch (IOException e) {
|
|
loge("Error to compressGzip due to " + e);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* decompress the gzip format data
|
|
*/
|
|
public static @Nullable byte[] decompressGzip(@NonNull byte[] data) {
|
|
if (data == null || data.length == 0) {
|
|
return data;
|
|
}
|
|
byte[] out = null;
|
|
try {
|
|
ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
|
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
GZIPInputStream gzipDecompressingStream =
|
|
new GZIPInputStream(inputStream);
|
|
byte[] buf = new byte[1024];
|
|
int size = gzipDecompressingStream.read(buf);
|
|
while (size >= 0) {
|
|
outputStream.write(buf, 0, size);
|
|
size = gzipDecompressingStream.read(buf);
|
|
}
|
|
gzipDecompressingStream.close();
|
|
inputStream.close();
|
|
out = outputStream.toByteArray();
|
|
outputStream.close();
|
|
} catch (IOException e) {
|
|
loge("Error to decompressGzip due to " + e);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* save the config to siminfo db. It is only used internally.
|
|
*/
|
|
public static void updateConfigForSub(@NonNull Context cxt, int subId,
|
|
@NonNull byte[] config, boolean isCompressed) {
|
|
//always store gzip compressed data
|
|
byte[] data = isCompressed ? config : compressGzip(config);
|
|
ContentValues values = new ContentValues();
|
|
values.put(SimInfo.COLUMN_RCS_CONFIG, data);
|
|
cxt.getContentResolver().update(SimInfo.CONTENT_URI, values,
|
|
SimInfo.COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID + "=" + subId, null);
|
|
}
|
|
|
|
/**
|
|
* load the config from siminfo db. It is only used internally.
|
|
*/
|
|
public static @Nullable byte[] loadRcsConfigForSub(@NonNull Context cxt,
|
|
int subId, boolean isCompressed) {
|
|
|
|
byte[] data = null;
|
|
|
|
Cursor cursor = cxt.getContentResolver().query(SimInfo.CONTENT_URI, null,
|
|
SimInfo.COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID + "=" + subId, null, null);
|
|
try {
|
|
if (cursor != null && cursor.moveToFirst()) {
|
|
data = cursor.getBlob(cursor.getColumnIndexOrThrow(SimInfo.COLUMN_RCS_CONFIG));
|
|
}
|
|
} catch (Exception e) {
|
|
loge("error to load rcs config for sub:" + subId + " due to " + e);
|
|
} finally {
|
|
if (cursor != null) {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return isCompressed ? data : decompressGzip(data);
|
|
}
|
|
|
|
private static void logd(String msg) {
|
|
Rlog.d(LOG_TAG, msg);
|
|
}
|
|
|
|
private static void loge(String msg) {
|
|
Rlog.e(LOG_TAG, msg);
|
|
}
|
|
}
|