blob: 051d96341cedc6a3a75c281589a6175403446b42 [file] [log] [blame]
Jordan Liudfcbfaf2019-10-11 11:42:03 -07001/*
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.cellbroadcastservice;
18
19import static com.android.internal.telephony.gsm.SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER;
20
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.Context;
24import android.database.Cursor;
25import android.net.Uri;
26import android.os.Message;
27import android.provider.Telephony.CellBroadcasts;
28import android.telephony.CellInfo;
29import android.telephony.CellLocation;
30import android.telephony.SmsCbLocation;
31import android.telephony.SmsCbMessage;
32import android.telephony.TelephonyManager;
33import android.telephony.gsm.GsmCellLocation;
34import android.text.format.DateUtils;
35
36import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage;
37import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
38import com.android.internal.telephony.CbGeoUtils.Geometry;
39
40import dalvik.annotation.compat.UnsupportedAppUsage;
41
42import java.util.ArrayList;
43import java.util.HashMap;
44import java.util.Iterator;
45import java.util.List;
46
47/**
48 * Handler for 3GPP format Cell Broadcasts. Parent class can also handle CDMA Cell Broadcasts.
49 */
50public class GsmCellBroadcastHandler extends CellBroadcastHandler {
51 private static final boolean VDBG = false; // log CB PDU data
52
53 /** Indicates that a message is not being broadcasted. */
54 private static final String MESSAGE_NOT_BROADCASTED = "0";
55
56 /** This map holds incomplete concatenated messages waiting for assembly. */
57 @UnsupportedAppUsage
58 private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
59 new HashMap<>(4);
60
61 protected GsmCellBroadcastHandler(Context context) {
62 super("GsmCellBroadcastHandler", context);
63 }
64
65 @Override
66 protected void onQuitting() {
67 super.onQuitting(); // release wakelock
68 }
69
70 /**
71 * Handle a GSM cell broadcast message passed from the telephony framework.
72 * @param message
73 */
74 public void onGsmCellBroadcastSms(int slotIndex, byte[] message) {
75 sendMessage(EVENT_NEW_SMS_MESSAGE, slotIndex, -1, message);
76 }
77
78 /**
79 * Create a new CellBroadcastHandler.
80 * @param context the context to use for dispatching Intents
81 * @return the new handler
82 */
83 public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context) {
84 GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context);
85 handler.start();
86 return handler;
87 }
88
89 /**
90 * Find the cell broadcast messages specify by the geo-fencing trigger message and perform a
91 * geo-fencing check for these messages.
92 * @param geoFencingTriggerMessage the trigger message
93 *
94 * @return {@code True} if geo-fencing is need for some cell broadcast message.
95 */
96 private boolean handleGeoFencingTriggerMessage(
97 GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex) {
98 final List<SmsCbMessage> cbMessages = new ArrayList<>();
99 final List<Uri> cbMessageUris = new ArrayList<>();
100
101 // Only consider the cell broadcast received within 24 hours.
102 long lastReceivedTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS;
103
104 // Find the cell broadcast message identify by the message identifier and serial number
105 // and is not broadcasted.
106 String where = CellBroadcasts.SERVICE_CATEGORY + "=? AND "
107 + CellBroadcasts.SERIAL_NUMBER + "=? AND "
108 + CellBroadcasts.MESSAGE_BROADCASTED + "=? AND "
109 + CellBroadcasts.RECEIVED_TIME + ">?";
110
111 ContentResolver resolver = mContext.getContentResolver();
112 for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) {
Chen Xu34c71672019-10-21 22:56:37 -0700113 try (Cursor cursor = resolver.query(CellBroadcasts.CONTENT_URI,
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700114 CellBroadcasts.QUERY_COLUMNS_FWK,
115 where,
116 new String[] { Integer.toString(identity.messageIdentifier),
117 Integer.toString(identity.serialNumber), MESSAGE_NOT_BROADCASTED,
118 Long.toString(lastReceivedTime) },
119 null /* sortOrder */)) {
120 if (cursor != null) {
121 while (cursor.moveToNext()) {
122 cbMessages.add(SmsCbMessage.createFromCursor(cursor));
Chen Xu34c71672019-10-21 22:56:37 -0700123 cbMessageUris.add(ContentUris.withAppendedId(CellBroadcasts.CONTENT_URI,
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700124 cursor.getInt(cursor.getColumnIndex(CellBroadcasts._ID))));
125 }
126 }
127 }
128 }
129
130 List<Geometry> commonBroadcastArea = new ArrayList<>();
131 if (geoFencingTriggerMessage.shouldShareBroadcastArea()) {
132 for (SmsCbMessage msg : cbMessages) {
133 if (msg.getGeometries() != null) {
134 commonBroadcastArea.addAll(msg.getGeometries());
135 }
136 }
137 }
138
139 // ATIS doesn't specify the geo fencing maximum wait time for the cell broadcasts specified
140 // in geo fencing trigger message. We will pick the largest maximum wait time among these
141 // cell broadcasts.
142 int maximumWaitTimeSec = 0;
143 for (SmsCbMessage msg : cbMessages) {
144 maximumWaitTimeSec = Math.max(maximumWaitTimeSec, msg.getMaximumWaitingTime());
145 }
146
147 if (DBG) {
148 logd("Geo-fencing trigger message = " + geoFencingTriggerMessage);
149 for (SmsCbMessage msg : cbMessages) {
150 logd(msg.toString());
151 }
152 }
153
154 if (cbMessages.isEmpty()) {
155 if (DBG) logd("No CellBroadcast message need to be broadcasted");
156 return false;
157 }
158
159 requestLocationUpdate(location -> {
160 if (location == null) {
161 // If the location is not available, broadcast the messages directly.
162 broadcastMessage(cbMessages, cbMessageUris, slotIndex);
163 } else {
164 for (int i = 0; i < cbMessages.size(); i++) {
165 List<Geometry> broadcastArea = !commonBroadcastArea.isEmpty()
166 ? commonBroadcastArea : cbMessages.get(i).getGeometries();
167 if (broadcastArea == null || broadcastArea.isEmpty()) {
168 broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
169 } else {
170 performGeoFencing(cbMessages.get(i), cbMessageUris.get(i), broadcastArea,
171 location, slotIndex);
172 }
173 }
174 }
175 }, maximumWaitTimeSec);
176 return true;
177 }
178
179 /**
180 * Handle 3GPP-format Cell Broadcast messages sent from radio.
181 *
182 * @param message the message to process
183 * @return true if need to wait for geo-fencing or an ordered broadcast was sent.
184 */
185 @Override
186 protected boolean handleSmsMessage(Message message) {
187 // For GSM, message.obj should be a byte[]
188 int slotIndex = message.arg1;
189 if (message.obj instanceof byte[]) {
190 byte[] pdu = (byte[]) message.obj;
191 SmsCbHeader header = createSmsCbHeader(pdu);
192 if (header == null) return false;
193
194 if (header.getServiceCategory() == MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) {
195 GeoFencingTriggerMessage triggerMessage =
196 GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu);
197 if (triggerMessage != null) {
198 return handleGeoFencingTriggerMessage(triggerMessage, slotIndex);
199 }
200 } else {
201 SmsCbMessage cbMessage = handleGsmBroadcastSms(header, pdu, slotIndex);
202 if (cbMessage != null) {
203 handleBroadcastSms(cbMessage);
204 return true;
205 }
206 if (VDBG) log("Not handled GSM broadcasts.");
207 }
208 }
209 return super.handleSmsMessage(message);
210 }
211
212 // return the cell location from the first returned cell info, prioritizing GSM
213 private CellLocation getCellLocation() {
214 TelephonyManager tm =
215 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
216 List<CellInfo> infos = tm.getAllCellInfo();
217 for (CellInfo info : infos) {
218 CellLocation cl = info.getCellIdentity().asCellLocation();
219 if (cl instanceof GsmCellLocation) {
220 return cl;
221 }
222 }
223 // If no GSM, return first in list
224 if (infos != null && !infos.isEmpty() && infos.get(0) != null) {
225 return infos.get(0).getCellIdentity().asCellLocation();
226 }
227 return CellLocation.getEmpty();
228 }
229
230
231 /**
232 * Handle 3GPP format SMS-CB message.
233 * @param header the cellbroadcast header.
234 * @param receivedPdu the received PDUs as a byte[]
235 */
236 private SmsCbMessage handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu,
237 int slotIndex) {
238 try {
239 if (VDBG) {
240 int pduLength = receivedPdu.length;
241 for (int i = 0; i < pduLength; i += 8) {
242 StringBuilder sb = new StringBuilder("SMS CB pdu data: ");
243 for (int j = i; j < i + 8 && j < pduLength; j++) {
244 int b = receivedPdu[j] & 0xff;
245 if (b < 0x10) {
246 sb.append('0');
247 }
248 sb.append(Integer.toHexString(b)).append(' ');
249 }
250 log(sb.toString());
251 }
252 }
253
254 if (VDBG) log("header=" + header);
255 TelephonyManager tm =
256 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
257 // TODO make a systemAPI for getNetworkOperatorForSlotIndex
258 String plmn = tm.getNetworkOperatorForPhone(slotIndex);
259 int lac = -1;
260 int cid = -1;
261 CellLocation cl = getCellLocation();
262 // Check if cell location is GsmCellLocation. This is required to support
263 // dual-mode devices such as CDMA/LTE devices that require support for
264 // both 3GPP and 3GPP2 format messages
265 if (cl instanceof GsmCellLocation) {
266 GsmCellLocation cellLocation = (GsmCellLocation) cl;
267 lac = cellLocation.getLac();
268 cid = cellLocation.getCid();
269 }
270
271 SmsCbLocation location;
272 switch (header.getGeographicalScope()) {
273 case SmsCbMessage.GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE:
274 location = new SmsCbLocation(plmn, lac, -1);
275 break;
276
277 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
278 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
279 location = new SmsCbLocation(plmn, lac, cid);
280 break;
281
282 case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
283 default:
284 location = new SmsCbLocation(plmn);
285 break;
286 }
287
288 byte[][] pdus;
289 int pageCount = header.getNumberOfPages();
290 if (pageCount > 1) {
291 // Multi-page message
292 SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location);
293
294 // Try to find other pages of the same message
295 pdus = mSmsCbPageMap.get(concatInfo);
296
297 if (pdus == null) {
298 // This is the first page of this message, make room for all
299 // pages and keep until complete
300 pdus = new byte[pageCount][];
301
302 mSmsCbPageMap.put(concatInfo, pdus);
303 }
304
305 if (VDBG) log("pdus size=" + pdus.length);
306 // Page parameter is one-based
307 pdus[header.getPageIndex() - 1] = receivedPdu;
308
309 for (byte[] pdu : pdus) {
310 if (pdu == null) {
311 // Still missing pages, exit
312 log("still missing pdu");
313 return null;
314 }
315 }
316
317 // Message complete, remove and dispatch
318 mSmsCbPageMap.remove(concatInfo);
319 } else {
320 // Single page message
321 pdus = new byte[1][];
322 pdus[0] = receivedPdu;
323 }
324
325 // Remove messages that are out of scope to prevent the map from
326 // growing indefinitely, containing incomplete messages that were
327 // never assembled
328 Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator();
329
330 while (iter.hasNext()) {
331 SmsCbConcatInfo info = iter.next();
332
333 if (!info.matchesLocation(plmn, lac, cid)) {
334 iter.remove();
335 }
336 }
337
338 return GsmSmsCbMessage.createSmsCbMessage(mContext, header, location, pdus, slotIndex);
339
340 } catch (RuntimeException e) {
341 loge("Error in decoding SMS CB pdu", e);
342 return null;
343 }
344 }
345
346 private SmsCbHeader createSmsCbHeader(byte[] bytes) {
347 try {
348 return new SmsCbHeader(bytes);
349 } catch (Exception ex) {
350 loge("Can't create SmsCbHeader, ex = " + ex.toString());
351 return null;
352 }
353 }
354
355 /**
356 * Holds all info about a message page needed to assemble a complete concatenated message.
357 */
358 private static final class SmsCbConcatInfo {
359
360 private final SmsCbHeader mHeader;
361 private final SmsCbLocation mLocation;
362
363 @UnsupportedAppUsage
364 SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) {
365 mHeader = header;
366 mLocation = location;
367 }
368
369 @Override
370 public int hashCode() {
371 return (mHeader.getSerialNumber() * 31) + mLocation.hashCode();
372 }
373
374 @Override
375 public boolean equals(Object obj) {
376 if (obj instanceof SmsCbConcatInfo) {
377 SmsCbConcatInfo other = (SmsCbConcatInfo) obj;
378
379 // Two pages match if they have the same serial number (which includes the
380 // geographical scope and update number), and both pages belong to the same
381 // location (PLMN, plus LAC and CID if these are part of the geographical scope).
382 return mHeader.getSerialNumber() == other.mHeader.getSerialNumber()
383 && mLocation.equals(other.mLocation);
384 }
385
386 return false;
387 }
388
389 /**
390 * Compare the location code for this message to the current location code. The match is
391 * relative to the geographical scope of the message, which determines whether the LAC
392 * and Cell ID are saved in mLocation or set to -1 to match all values.
393 *
394 * @param plmn the current PLMN
395 * @param lac the current Location Area (GSM) or Service Area (UMTS)
396 * @param cid the current Cell ID
397 * @return true if this message is valid for the current location; false otherwise
398 */
399 @UnsupportedAppUsage
400 public boolean matchesLocation(String plmn, int lac, int cid) {
401 return mLocation.isInLocationArea(plmn, lac, cid);
402 }
403 }
404}