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