1/*
2 * Copyright (C) 2013 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.gsm;
18
19import android.content.Context;
20import android.os.AsyncResult;
21import android.os.Message;
22import android.telephony.CellLocation;
23import android.telephony.SmsCbLocation;
24import android.telephony.SmsCbMessage;
25import android.telephony.TelephonyManager;
26import android.telephony.gsm.GsmCellLocation;
27
28import com.android.internal.telephony.CellBroadcastHandler;
29import com.android.internal.telephony.Phone;
30
31import java.util.HashMap;
32import java.util.Iterator;
33
34/**
35 * Handler for 3GPP format Cell Broadcasts. Parent class can also handle CDMA Cell Broadcasts.
36 */
37public class GsmCellBroadcastHandler extends CellBroadcastHandler {
38    private static final boolean VDBG = false;  // log CB PDU data
39
40    /** This map holds incomplete concatenated messages waiting for assembly. */
41    private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
42            new HashMap<SmsCbConcatInfo, byte[][]>(4);
43
44    protected GsmCellBroadcastHandler(Context context, Phone phone) {
45        super("GsmCellBroadcastHandler", context, phone);
46        phone.mCi.setOnNewGsmBroadcastSms(getHandler(), EVENT_NEW_SMS_MESSAGE, null);
47    }
48
49    @Override
50    protected void onQuitting() {
51        mPhone.mCi.unSetOnNewGsmBroadcastSms(getHandler());
52        super.onQuitting();     // release wakelock
53    }
54
55    /**
56     * Create a new CellBroadcastHandler.
57     * @param context the context to use for dispatching Intents
58     * @return the new handler
59     */
60    public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context,
61            Phone phone) {
62        GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, phone);
63        handler.start();
64        return handler;
65    }
66
67    /**
68     * Handle 3GPP-format Cell Broadcast messages sent from radio.
69     *
70     * @param message the message to process
71     * @return true if an ordered broadcast was sent; false on failure
72     */
73    @Override
74    protected boolean handleSmsMessage(Message message) {
75        if (message.obj instanceof AsyncResult) {
76            SmsCbMessage cbMessage = handleGsmBroadcastSms((AsyncResult) message.obj);
77            if (cbMessage != null) {
78                handleBroadcastSms(cbMessage);
79                return true;
80            }
81            if (VDBG) log("Not handled GSM broadcasts.");
82        }
83        return super.handleSmsMessage(message);
84    }
85
86    /**
87     * Handle 3GPP format SMS-CB message.
88     * @param ar the AsyncResult containing the received PDUs
89     */
90    private SmsCbMessage handleGsmBroadcastSms(AsyncResult ar) {
91        try {
92            byte[] receivedPdu = (byte[]) ar.result;
93
94            if (VDBG) {
95                int pduLength = receivedPdu.length;
96                for (int i = 0; i < pduLength; i += 8) {
97                    StringBuilder sb = new StringBuilder("SMS CB pdu data: ");
98                    for (int j = i; j < i + 8 && j < pduLength; j++) {
99                        int b = receivedPdu[j] & 0xff;
100                        if (b < 0x10) {
101                            sb.append('0');
102                        }
103                        sb.append(Integer.toHexString(b)).append(' ');
104                    }
105                    log(sb.toString());
106                }
107            }
108
109            SmsCbHeader header = new SmsCbHeader(receivedPdu);
110            if (VDBG) log("header=" + header);
111            String plmn = TelephonyManager.from(mContext).getNetworkOperatorForPhone(
112                    mPhone.getPhoneId());
113            int lac = -1;
114            int cid = -1;
115            CellLocation cl = mPhone.getCellLocation();
116            // Check if cell location is GsmCellLocation.  This is required to support
117            // dual-mode devices such as CDMA/LTE devices that require support for
118            // both 3GPP and 3GPP2 format messages
119            if (cl instanceof GsmCellLocation) {
120                GsmCellLocation cellLocation = (GsmCellLocation)cl;
121                lac = cellLocation.getLac();
122                cid = cellLocation.getCid();
123            }
124
125            SmsCbLocation location;
126            switch (header.getGeographicalScope()) {
127                case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE:
128                    location = new SmsCbLocation(plmn, lac, -1);
129                    break;
130
131                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
132                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
133                    location = new SmsCbLocation(plmn, lac, cid);
134                    break;
135
136                case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
137                default:
138                    location = new SmsCbLocation(plmn);
139                    break;
140            }
141
142            byte[][] pdus;
143            int pageCount = header.getNumberOfPages();
144            if (pageCount > 1) {
145                // Multi-page message
146                SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location);
147
148                // Try to find other pages of the same message
149                pdus = mSmsCbPageMap.get(concatInfo);
150
151                if (pdus == null) {
152                    // This is the first page of this message, make room for all
153                    // pages and keep until complete
154                    pdus = new byte[pageCount][];
155
156                    mSmsCbPageMap.put(concatInfo, pdus);
157                }
158
159                if (VDBG) log("pdus size=" + pdus.length);
160                // Page parameter is one-based
161                pdus[header.getPageIndex() - 1] = receivedPdu;
162
163                for (byte[] pdu : pdus) {
164                    if (pdu == null) {
165                        // Still missing pages, exit
166                        log("still missing pdu");
167                        return null;
168                    }
169                }
170
171                // Message complete, remove and dispatch
172                mSmsCbPageMap.remove(concatInfo);
173            } else {
174                // Single page message
175                pdus = new byte[1][];
176                pdus[0] = receivedPdu;
177            }
178
179            // Remove messages that are out of scope to prevent the map from
180            // growing indefinitely, containing incomplete messages that were
181            // never assembled
182            Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator();
183
184            while (iter.hasNext()) {
185                SmsCbConcatInfo info = iter.next();
186
187                if (!info.matchesLocation(plmn, lac, cid)) {
188                    iter.remove();
189                }
190            }
191
192            return GsmSmsCbMessage.createSmsCbMessage(mContext, header, location, pdus);
193
194        } catch (RuntimeException e) {
195            loge("Error in decoding SMS CB pdu", e);
196            return null;
197        }
198    }
199
200    /**
201     * Holds all info about a message page needed to assemble a complete concatenated message.
202     */
203    private static final class SmsCbConcatInfo {
204
205        private final SmsCbHeader mHeader;
206        private final SmsCbLocation mLocation;
207
208        SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) {
209            mHeader = header;
210            mLocation = location;
211        }
212
213        @Override
214        public int hashCode() {
215            return (mHeader.getSerialNumber() * 31) + mLocation.hashCode();
216        }
217
218        @Override
219        public boolean equals(Object obj) {
220            if (obj instanceof SmsCbConcatInfo) {
221                SmsCbConcatInfo other = (SmsCbConcatInfo)obj;
222
223                // Two pages match if they have the same serial number (which includes the
224                // geographical scope and update number), and both pages belong to the same
225                // location (PLMN, plus LAC and CID if these are part of the geographical scope).
226                return mHeader.getSerialNumber() == other.mHeader.getSerialNumber()
227                        && mLocation.equals(other.mLocation);
228            }
229
230            return false;
231        }
232
233        /**
234         * Compare the location code for this message to the current location code. The match is
235         * relative to the geographical scope of the message, which determines whether the LAC
236         * and Cell ID are saved in mLocation or set to -1 to match all values.
237         *
238         * @param plmn the current PLMN
239         * @param lac the current Location Area (GSM) or Service Area (UMTS)
240         * @param cid the current Cell ID
241         * @return true if this message is valid for the current location; false otherwise
242         */
243        public boolean matchesLocation(String plmn, int lac, int cid) {
244            return mLocation.isInLocationArea(plmn, lac, cid);
245        }
246    }
247}
248