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