blob: 6aee320c1a455722b0cd466e33173b19f108a777 [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
Jordan Liu62e183f2019-12-20 15:09:44 -080019import static com.android.cellbroadcastservice.CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__GSM_INVALID_PDU;
20import static com.android.cellbroadcastservice.CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__UNEXPECTED_GSM_MESSAGE_TYPE_FROM_FWK;
21
Jordan Liudfcbfaf2019-10-11 11:42:03 -070022import android.content.ContentResolver;
23import android.content.ContentUris;
24import android.content.Context;
Jack Yu7afa1c02019-11-07 22:59:46 -080025import android.content.res.Resources;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070026import android.database.Cursor;
27import android.net.Uri;
Jack Yu7afa1c02019-11-07 22:59:46 -080028import android.os.Looper;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070029import android.os.Message;
Jack Yu7afa1c02019-11-07 22:59:46 -080030import android.os.SystemClock;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070031import android.provider.Telephony.CellBroadcasts;
Jordan Liu8d7d0a12019-10-21 12:30:56 -070032import android.telephony.CbGeoUtils.Geometry;
33import android.telephony.CellIdentity;
34import android.telephony.CellIdentityGsm;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070035import android.telephony.CellInfo;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070036import android.telephony.SmsCbLocation;
37import android.telephony.SmsCbMessage;
Jack Yu7afa1c02019-11-07 22:59:46 -080038import android.telephony.SubscriptionManager;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070039import android.telephony.TelephonyManager;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070040import android.text.format.DateUtils;
Jordan Liu8d7d0a12019-10-21 12:30:56 -070041import android.util.Pair;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070042
43import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage;
44import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
Jack Yu7afa1c02019-11-07 22:59:46 -080045import com.android.internal.annotations.VisibleForTesting;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070046
Jack Yu7afa1c02019-11-07 22:59:46 -080047import java.text.DateFormat;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070048import java.util.ArrayList;
49import java.util.HashMap;
50import java.util.Iterator;
51import java.util.List;
52
53/**
54 * Handler for 3GPP format Cell Broadcasts. Parent class can also handle CDMA Cell Broadcasts.
55 */
56public class GsmCellBroadcastHandler extends CellBroadcastHandler {
57 private static final boolean VDBG = false; // log CB PDU data
58
59 /** Indicates that a message is not being broadcasted. */
60 private static final String MESSAGE_NOT_BROADCASTED = "0";
61
62 /** This map holds incomplete concatenated messages waiting for assembly. */
Jordan Liudfcbfaf2019-10-11 11:42:03 -070063 private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
64 new HashMap<>(4);
65
Jack Yu7afa1c02019-11-07 22:59:46 -080066 @VisibleForTesting
Jordan Liu997ab0a2019-11-14 14:23:49 -080067 public GsmCellBroadcastHandler(Context context, Looper looper) {
Jack Yu7afa1c02019-11-07 22:59:46 -080068 super("GsmCellBroadcastHandler", context, looper);
Jordan Liudfcbfaf2019-10-11 11:42:03 -070069 }
70
71 @Override
72 protected void onQuitting() {
73 super.onQuitting(); // release wakelock
74 }
75
76 /**
77 * Handle a GSM cell broadcast message passed from the telephony framework.
78 * @param message
79 */
80 public void onGsmCellBroadcastSms(int slotIndex, byte[] message) {
81 sendMessage(EVENT_NEW_SMS_MESSAGE, slotIndex, -1, message);
82 }
83
84 /**
85 * Create a new CellBroadcastHandler.
86 * @param context the context to use for dispatching Intents
87 * @return the new handler
88 */
89 public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context) {
Jack Yu7afa1c02019-11-07 22:59:46 -080090 GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, Looper.myLooper());
Jordan Liudfcbfaf2019-10-11 11:42:03 -070091 handler.start();
92 return handler;
93 }
94
95 /**
96 * Find the cell broadcast messages specify by the geo-fencing trigger message and perform a
97 * geo-fencing check for these messages.
98 * @param geoFencingTriggerMessage the trigger message
99 *
100 * @return {@code True} if geo-fencing is need for some cell broadcast message.
101 */
102 private boolean handleGeoFencingTriggerMessage(
103 GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex) {
104 final List<SmsCbMessage> cbMessages = new ArrayList<>();
105 final List<Uri> cbMessageUris = new ArrayList<>();
106
Jack Yu7afa1c02019-11-07 22:59:46 -0800107 SubscriptionManager subMgr = (SubscriptionManager) mContext.getSystemService(
108 Context.TELEPHONY_SUBSCRIPTION_SERVICE);
109 int[] subIds = subMgr.getSubscriptionIds(slotIndex);
110 Resources res;
111 if (subIds != null) {
112 res = getResources(subIds[0]);
113 } else {
114 res = getResources(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
115 }
116
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700117 // Only consider the cell broadcast received within 24 hours.
118 long lastReceivedTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS;
119
Jack Yu7afa1c02019-11-07 22:59:46 -0800120 // Some carriers require reset duplication detection after airplane mode or reboot.
121 if (res.getBoolean(R.bool.reset_on_power_cycle_or_airplane_mode)) {
122 lastReceivedTime = Long.max(lastReceivedTime, mLastAirplaneModeTime);
123 lastReceivedTime = Long.max(lastReceivedTime,
124 System.currentTimeMillis() - SystemClock.elapsedRealtime());
125 }
126
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700127 // Find the cell broadcast message identify by the message identifier and serial number
128 // and is not broadcasted.
129 String where = CellBroadcasts.SERVICE_CATEGORY + "=? AND "
130 + CellBroadcasts.SERIAL_NUMBER + "=? AND "
131 + CellBroadcasts.MESSAGE_BROADCASTED + "=? AND "
132 + CellBroadcasts.RECEIVED_TIME + ">?";
133
134 ContentResolver resolver = mContext.getContentResolver();
135 for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) {
Chen Xu34c71672019-10-21 22:56:37 -0700136 try (Cursor cursor = resolver.query(CellBroadcasts.CONTENT_URI,
Jordan Liub51f6932019-11-15 11:13:59 -0800137 CellBroadcastProvider.QUERY_COLUMNS,
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700138 where,
139 new String[] { Integer.toString(identity.messageIdentifier),
140 Integer.toString(identity.serialNumber), MESSAGE_NOT_BROADCASTED,
141 Long.toString(lastReceivedTime) },
142 null /* sortOrder */)) {
143 if (cursor != null) {
144 while (cursor.moveToNext()) {
145 cbMessages.add(SmsCbMessage.createFromCursor(cursor));
Chen Xu34c71672019-10-21 22:56:37 -0700146 cbMessageUris.add(ContentUris.withAppendedId(CellBroadcasts.CONTENT_URI,
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700147 cursor.getInt(cursor.getColumnIndex(CellBroadcasts._ID))));
148 }
149 }
150 }
151 }
152
Jack Yu7afa1c02019-11-07 22:59:46 -0800153 log("Found " + cbMessages.size() + " not broadcasted messages since "
154 + DateFormat.getDateTimeInstance().format(lastReceivedTime));
155
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700156 List<Geometry> commonBroadcastArea = new ArrayList<>();
157 if (geoFencingTriggerMessage.shouldShareBroadcastArea()) {
158 for (SmsCbMessage msg : cbMessages) {
159 if (msg.getGeometries() != null) {
160 commonBroadcastArea.addAll(msg.getGeometries());
161 }
162 }
163 }
164
165 // ATIS doesn't specify the geo fencing maximum wait time for the cell broadcasts specified
166 // in geo fencing trigger message. We will pick the largest maximum wait time among these
167 // cell broadcasts.
168 int maximumWaitTimeSec = 0;
169 for (SmsCbMessage msg : cbMessages) {
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700170 maximumWaitTimeSec = Math.max(maximumWaitTimeSec, msg.getMaximumWaitingDuration());
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700171 }
172
173 if (DBG) {
174 logd("Geo-fencing trigger message = " + geoFencingTriggerMessage);
175 for (SmsCbMessage msg : cbMessages) {
176 logd(msg.toString());
177 }
178 }
179
180 if (cbMessages.isEmpty()) {
181 if (DBG) logd("No CellBroadcast message need to be broadcasted");
182 return false;
183 }
184
185 requestLocationUpdate(location -> {
186 if (location == null) {
187 // If the location is not available, broadcast the messages directly.
Jack Yu8cb5d152019-10-23 11:35:32 -0700188 for (int i = 0; i < cbMessages.size(); i++) {
189 broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
190 }
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700191 } else {
192 for (int i = 0; i < cbMessages.size(); i++) {
193 List<Geometry> broadcastArea = !commonBroadcastArea.isEmpty()
194 ? commonBroadcastArea : cbMessages.get(i).getGeometries();
195 if (broadcastArea == null || broadcastArea.isEmpty()) {
196 broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
197 } else {
198 performGeoFencing(cbMessages.get(i), cbMessageUris.get(i), broadcastArea,
199 location, slotIndex);
200 }
201 }
202 }
203 }, maximumWaitTimeSec);
204 return true;
205 }
206
207 /**
208 * Handle 3GPP-format Cell Broadcast messages sent from radio.
209 *
210 * @param message the message to process
211 * @return true if need to wait for geo-fencing or an ordered broadcast was sent.
212 */
213 @Override
214 protected boolean handleSmsMessage(Message message) {
215 // For GSM, message.obj should be a byte[]
216 int slotIndex = message.arg1;
217 if (message.obj instanceof byte[]) {
218 byte[] pdu = (byte[]) message.obj;
219 SmsCbHeader header = createSmsCbHeader(pdu);
220 if (header == null) return false;
221
Jack Yu7afa1c02019-11-07 22:59:46 -0800222 log("header=" + header);
Jordan Liu5e174552019-11-07 11:54:10 -0800223 if (header.getServiceCategory() == SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) {
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700224 GeoFencingTriggerMessage triggerMessage =
225 GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu);
226 if (triggerMessage != null) {
227 return handleGeoFencingTriggerMessage(triggerMessage, slotIndex);
228 }
229 } else {
230 SmsCbMessage cbMessage = handleGsmBroadcastSms(header, pdu, slotIndex);
231 if (cbMessage != null) {
Jack Yu2390cad2019-10-24 00:19:49 -0700232 if (isDuplicate(cbMessage)) {
233 return false;
234 }
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700235 handleBroadcastSms(cbMessage);
236 return true;
237 }
238 if (VDBG) log("Not handled GSM broadcasts.");
239 }
Jordan Liu62e183f2019-12-20 15:09:44 -0800240 } else {
241 loge("handleSmsMessage for GSM got object of type: "
242 + message.obj.getClass().getName());
243 CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR,
244 CELL_BROADCAST_MESSAGE_ERROR__TYPE__UNEXPECTED_GSM_MESSAGE_TYPE_FROM_FWK,
245 message.obj.getClass().getName());
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700246 }
Jordan Liu62e183f2019-12-20 15:09:44 -0800247 if (message.obj instanceof SmsCbMessage) {
248 return super.handleSmsMessage(message);
249 } else {
250 return false;
251 }
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700252 }
253
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700254 // return the GSM cell location from the first GSM cell info
255 private Pair<Integer, Integer> getGsmLacAndCid() {
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700256 TelephonyManager tm =
257 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
258 List<CellInfo> infos = tm.getAllCellInfo();
259 for (CellInfo info : infos) {
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700260 CellIdentity ci = info.getCellIdentity();
261 if (ci instanceof CellIdentityGsm) {
262 CellIdentityGsm ciGsm = (CellIdentityGsm) ci;
263 int lac = ciGsm.getLac() != CellInfo.UNAVAILABLE ? ciGsm.getLac() : -1;
264 int cid = ciGsm.getCid() != CellInfo.UNAVAILABLE ? ciGsm.getCid() : -1;
265 return Pair.create(lac, cid);
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700266 }
267 }
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700268 return null;
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700269 }
270
271
272 /**
273 * Handle 3GPP format SMS-CB message.
274 * @param header the cellbroadcast header.
275 * @param receivedPdu the received PDUs as a byte[]
276 */
277 private SmsCbMessage handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu,
278 int slotIndex) {
279 try {
280 if (VDBG) {
281 int pduLength = receivedPdu.length;
282 for (int i = 0; i < pduLength; i += 8) {
283 StringBuilder sb = new StringBuilder("SMS CB pdu data: ");
284 for (int j = i; j < i + 8 && j < pduLength; j++) {
285 int b = receivedPdu[j] & 0xff;
286 if (b < 0x10) {
287 sb.append('0');
288 }
289 sb.append(Integer.toHexString(b)).append(' ');
290 }
291 log(sb.toString());
292 }
293 }
294
295 if (VDBG) log("header=" + header);
296 TelephonyManager tm =
297 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
Jordan Liuac12bae2019-11-21 11:59:07 -0800298 tm.createForSubscriptionId(getSubIdForPhone(mContext, slotIndex));
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700299 // TODO make a systemAPI for getNetworkOperatorForSlotIndex
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700300 String plmn = tm.getSimOperator();
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700301 int lac = -1;
302 int cid = -1;
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700303 Pair<Integer, Integer> lacAndCid = getGsmLacAndCid();
304 // Check if GSM lac and cid are available. This is required to support
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700305 // dual-mode devices such as CDMA/LTE devices that require support for
306 // both 3GPP and 3GPP2 format messages
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700307 if (lacAndCid != null) {
308 lac = lacAndCid.first;
309 cid = lacAndCid.second;
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700310 }
311
312 SmsCbLocation location;
313 switch (header.getGeographicalScope()) {
314 case SmsCbMessage.GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE:
315 location = new SmsCbLocation(plmn, lac, -1);
316 break;
317
318 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
319 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
320 location = new SmsCbLocation(plmn, lac, cid);
321 break;
322
323 case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
324 default:
Jordan Liuac12bae2019-11-21 11:59:07 -0800325 location = new SmsCbLocation(plmn, -1, -1);
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700326 break;
327 }
328
329 byte[][] pdus;
330 int pageCount = header.getNumberOfPages();
331 if (pageCount > 1) {
332 // Multi-page message
333 SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location);
334
335 // Try to find other pages of the same message
336 pdus = mSmsCbPageMap.get(concatInfo);
337
338 if (pdus == null) {
339 // This is the first page of this message, make room for all
340 // pages and keep until complete
341 pdus = new byte[pageCount][];
342
343 mSmsCbPageMap.put(concatInfo, pdus);
344 }
345
346 if (VDBG) log("pdus size=" + pdus.length);
347 // Page parameter is one-based
348 pdus[header.getPageIndex() - 1] = receivedPdu;
349
350 for (byte[] pdu : pdus) {
351 if (pdu == null) {
352 // Still missing pages, exit
353 log("still missing pdu");
354 return null;
355 }
356 }
357
358 // Message complete, remove and dispatch
359 mSmsCbPageMap.remove(concatInfo);
360 } else {
361 // Single page message
362 pdus = new byte[1][];
363 pdus[0] = receivedPdu;
364 }
365
366 // Remove messages that are out of scope to prevent the map from
367 // growing indefinitely, containing incomplete messages that were
368 // never assembled
369 Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator();
370
371 while (iter.hasNext()) {
372 SmsCbConcatInfo info = iter.next();
373
374 if (!info.matchesLocation(plmn, lac, cid)) {
375 iter.remove();
376 }
377 }
378
379 return GsmSmsCbMessage.createSmsCbMessage(mContext, header, location, pdus, slotIndex);
380
381 } catch (RuntimeException e) {
382 loge("Error in decoding SMS CB pdu", e);
Jordan Liu62e183f2019-12-20 15:09:44 -0800383 CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR,
384 CELL_BROADCAST_MESSAGE_ERROR__TYPE__GSM_INVALID_PDU, e.toString());
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700385 return null;
386 }
387 }
388
389 private SmsCbHeader createSmsCbHeader(byte[] bytes) {
390 try {
391 return new SmsCbHeader(bytes);
392 } catch (Exception ex) {
393 loge("Can't create SmsCbHeader, ex = " + ex.toString());
394 return null;
395 }
396 }
397
398 /**
399 * Holds all info about a message page needed to assemble a complete concatenated message.
400 */
401 private static final class SmsCbConcatInfo {
402
403 private final SmsCbHeader mHeader;
404 private final SmsCbLocation mLocation;
405
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700406 SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) {
407 mHeader = header;
408 mLocation = location;
409 }
410
411 @Override
412 public int hashCode() {
413 return (mHeader.getSerialNumber() * 31) + mLocation.hashCode();
414 }
415
416 @Override
417 public boolean equals(Object obj) {
418 if (obj instanceof SmsCbConcatInfo) {
419 SmsCbConcatInfo other = (SmsCbConcatInfo) obj;
420
421 // Two pages match if they have the same serial number (which includes the
422 // geographical scope and update number), and both pages belong to the same
423 // location (PLMN, plus LAC and CID if these are part of the geographical scope).
424 return mHeader.getSerialNumber() == other.mHeader.getSerialNumber()
425 && mLocation.equals(other.mLocation);
426 }
427
428 return false;
429 }
430
431 /**
432 * Compare the location code for this message to the current location code. The match is
433 * relative to the geographical scope of the message, which determines whether the LAC
434 * and Cell ID are saved in mLocation or set to -1 to match all values.
435 *
436 * @param plmn the current PLMN
437 * @param lac the current Location Area (GSM) or Service Area (UMTS)
438 * @param cid the current Cell ID
439 * @return true if this message is valid for the current location; false otherwise
440 */
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700441 public boolean matchesLocation(String plmn, int lac, int cid) {
442 return mLocation.isInLocationArea(plmn, lac, cid);
443 }
444 }
445}