1/*
2 * Copyright (C) 2014 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
17package com.android.internal.telephony;
18
19import android.telephony.Rlog;
20import android.os.Build;
21import android.util.SparseIntArray;
22import android.content.res.Resources;
23import android.content.res.XmlResourceParser;
24import android.telephony.SmsManager;
25import android.telephony.TelephonyManager;
26
27import com.android.internal.util.XmlUtils;
28import com.android.internal.telephony.cdma.sms.UserData;
29
30import org.xmlpull.v1.XmlPullParser;
31import org.xmlpull.v1.XmlPullParserException;
32
33public class Sms7BitEncodingTranslator {
34    private static final String TAG = "Sms7BitEncodingTranslator";
35    private static final boolean DBG = Build.IS_DEBUGGABLE ;
36    private static boolean mIs7BitTranslationTableLoaded = false;
37    private static SparseIntArray mTranslationTable = null;
38    private static SparseIntArray mTranslationTableCommon = null;
39    private static SparseIntArray mTranslationTableGSM = null;
40    private static SparseIntArray mTranslationTableCDMA = null;
41
42    // Parser variables
43    private static final String XML_START_TAG = "SmsEnforce7BitTranslationTable";
44    private static final String XML_TRANSLATION_TYPE_TAG = "TranslationType";
45    private static final String XML_CHARACTOR_TAG = "Character";
46    private static final String XML_FROM_TAG = "from";
47    private static final String XML_TO_TAG = "to";
48
49    /**
50     * Translates each message character that is not supported by GSM 7bit
51     * alphabet into a supported one
52     *
53     * @param message
54     *            message to be translated
55     * @param throwsException
56     *            if true and some error occurs during translation, an exception
57     *            is thrown; otherwise a null String is returned
58     * @return translated message or null if some error occur
59     */
60    public static String translate(CharSequence message) {
61        if (message == null) {
62            Rlog.w(TAG, "Null message can not be translated");
63            return null;
64        }
65
66        int size = message.length();
67        if (size <= 0) {
68            return "";
69        }
70
71        if (!mIs7BitTranslationTableLoaded) {
72            mTranslationTableCommon = new SparseIntArray();
73            mTranslationTableGSM = new SparseIntArray();
74            mTranslationTableCDMA = new SparseIntArray();
75            load7BitTranslationTableFromXml();
76            mIs7BitTranslationTableLoaded = true;
77        }
78
79        if ((mTranslationTableCommon != null && mTranslationTableCommon.size() > 0) ||
80                (mTranslationTableGSM != null && mTranslationTableGSM.size() > 0) ||
81                (mTranslationTableCDMA != null && mTranslationTableCDMA.size() > 0)) {
82            char[] output = new char[size];
83            boolean isCdmaFormat = useCdmaFormatForMoSms();
84            for (int i = 0; i < size; i++) {
85                output[i] = translateIfNeeded(message.charAt(i), isCdmaFormat);
86            }
87
88            return String.valueOf(output);
89        }
90
91        return null;
92    }
93
94    /**
95     * Translates a single character into its corresponding acceptable one, if
96     * needed, based on GSM 7-bit alphabet
97     *
98     * @param c
99     *            character to be translated
100     * @return original character, if it's present on GSM 7-bit alphabet; a
101     *         corresponding character, based on the translation table or white
102     *         space, if no mapping is found in the translation table for such
103     *         character
104     */
105    private static char translateIfNeeded(char c, boolean isCdmaFormat) {
106        if (noTranslationNeeded(c, isCdmaFormat)) {
107            if (DBG) {
108                Rlog.v(TAG, "No translation needed for " + Integer.toHexString(c));
109            }
110            return c;
111        }
112
113        /*
114         * Trying to translate unicode to Gsm 7-bit alphabet; If c is not
115         * present on translation table, c does not belong to Unicode Latin-1
116         * (Basic + Supplement), so we don't know how to translate it to a Gsm
117         * 7-bit character! We replace c for an empty space and advises the user
118         * about it.
119         */
120        int translation = -1;
121
122        if (mTranslationTableCommon != null) {
123            translation = mTranslationTableCommon.get(c, -1);
124        }
125
126        if (translation == -1) {
127            if (isCdmaFormat) {
128                if (mTranslationTableCDMA != null) {
129                    translation = mTranslationTableCDMA.get(c, -1);
130                }
131            } else {
132                if (mTranslationTableGSM != null) {
133                    translation = mTranslationTableGSM.get(c, -1);
134                }
135            }
136        }
137
138        if (translation != -1) {
139            if (DBG) {
140                Rlog.v(TAG, Integer.toHexString(c) + " (" + c + ")" + " translated to "
141                        + Integer.toHexString(translation) + " (" + (char) translation + ")");
142            }
143            return (char) translation;
144        } else {
145            if (DBG) {
146                Rlog.w(TAG, "No translation found for " + Integer.toHexString(c)
147                        + "! Replacing for empty space");
148            }
149            return ' ';
150        }
151    }
152
153    private static boolean noTranslationNeeded(char c, boolean isCdmaFormat) {
154        if (isCdmaFormat) {
155            return GsmAlphabet.isGsmSeptets(c) && UserData.charToAscii.get(c, -1) != -1;
156        }
157        else {
158            return GsmAlphabet.isGsmSeptets(c);
159        }
160    }
161
162    private static boolean useCdmaFormatForMoSms() {
163        if (!SmsManager.getDefault().isImsSmsSupported()) {
164            // use Voice technology to determine SMS format.
165            return TelephonyManager.getDefault().getCurrentPhoneType()
166                    == PhoneConstants.PHONE_TYPE_CDMA;
167        }
168        // IMS is registered with SMS support, check the SMS format supported
169        return (SmsConstants.FORMAT_3GPP2.equals(SmsManager.getDefault().getImsSmsFormat()));
170    }
171
172    /**
173     * Load the whole translation table file from the framework resource
174     * encoded in XML.
175     */
176    private static void load7BitTranslationTableFromXml() {
177        XmlResourceParser parser = null;
178        Resources r = Resources.getSystem();
179
180        if (parser == null) {
181            if (DBG) Rlog.d(TAG, "load7BitTranslationTableFromXml: open normal file");
182            parser = r.getXml(com.android.internal.R.xml.sms_7bit_translation_table);
183        }
184
185        try {
186            XmlUtils.beginDocument(parser, XML_START_TAG);
187            while (true)  {
188                XmlUtils.nextElement(parser);
189                String tag = parser.getName();
190                if (DBG) {
191                    Rlog.d(TAG, "tag: " + tag);
192                }
193                if (XML_TRANSLATION_TYPE_TAG.equals(tag)) {
194                    String type = parser.getAttributeValue(null, "Type");
195                    if (DBG) {
196                        Rlog.d(TAG, "type: " + type);
197                    }
198                    if (type.equals("common")) {
199                        mTranslationTable = mTranslationTableCommon;
200                    } else if (type.equals("gsm")) {
201                        mTranslationTable = mTranslationTableGSM;
202                    } else if (type.equals("cdma")) {
203                        mTranslationTable = mTranslationTableCDMA;
204                    } else {
205                        Rlog.e(TAG, "Error Parsing 7BitTranslationTable: found incorrect type" + type);
206                    }
207                } else if (XML_CHARACTOR_TAG.equals(tag) && mTranslationTable != null) {
208                    int from = parser.getAttributeUnsignedIntValue(null,
209                            XML_FROM_TAG, -1);
210                    int to = parser.getAttributeUnsignedIntValue(null,
211                            XML_TO_TAG, -1);
212                    if ((from != -1) && (to != -1)) {
213                        if (DBG) {
214                            Rlog.d(TAG, "Loading mapping " + Integer.toHexString(from)
215                                    .toUpperCase() + " -> " + Integer.toHexString(to)
216                                    .toUpperCase());
217                        }
218                        mTranslationTable.put (from, to);
219                    } else {
220                        Rlog.d(TAG, "Invalid translation table file format");
221                    }
222                } else {
223                    break;
224                }
225            }
226            if (DBG) Rlog.d(TAG, "load7BitTranslationTableFromXml: parsing successful, file loaded");
227        } catch (Exception e) {
228            Rlog.e(TAG, "Got exception while loading 7BitTranslationTable file.", e);
229        } finally {
230            if (parser instanceof XmlResourceParser) {
231                ((XmlResourceParser)parser).close();
232            }
233        }
234    }
235}
236