blob: fb04fa02f1793f36a0033a21673d6f2d6aec9692 [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 Liudfcbfaf2019-10-11 11:42:03 -070019import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.Context;
Jack Yu7afa1c02019-11-07 22:59:46 -080022import android.content.res.Resources;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070023import android.database.Cursor;
24import android.net.Uri;
Jack Yu7afa1c02019-11-07 22:59:46 -080025import android.os.Looper;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070026import android.os.Message;
Jack Yu7afa1c02019-11-07 22:59:46 -080027import android.os.SystemClock;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070028import android.provider.Telephony.CellBroadcasts;
Jordan Liu8d7d0a12019-10-21 12:30:56 -070029import android.telephony.CbGeoUtils.Geometry;
30import android.telephony.CellIdentity;
31import android.telephony.CellIdentityGsm;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070032import android.telephony.CellInfo;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070033import android.telephony.SmsCbLocation;
34import android.telephony.SmsCbMessage;
Jack Yu7afa1c02019-11-07 22:59:46 -080035import android.telephony.SubscriptionManager;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070036import android.telephony.TelephonyManager;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070037import android.text.format.DateUtils;
Jordan Liu8d7d0a12019-10-21 12:30:56 -070038import android.util.Pair;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070039
40import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage;
41import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
Jack Yu7afa1c02019-11-07 22:59:46 -080042import com.android.internal.annotations.VisibleForTesting;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070043
Jack Yu7afa1c02019-11-07 22:59:46 -080044import java.text.DateFormat;
Jordan Liudfcbfaf2019-10-11 11:42:03 -070045import java.util.ArrayList;
46import java.util.HashMap;
47import java.util.Iterator;
48import java.util.List;
49
50/**
51 * Handler for 3GPP format Cell Broadcasts. Parent class can also handle CDMA Cell Broadcasts.
52 */
53public class GsmCellBroadcastHandler extends CellBroadcastHandler {
54 private static final boolean VDBG = false; // log CB PDU data
55
56 /** Indicates that a message is not being broadcasted. */
57 private static final String MESSAGE_NOT_BROADCASTED = "0";
58
59 /** This map holds incomplete concatenated messages waiting for assembly. */
Jordan Liudfcbfaf2019-10-11 11:42:03 -070060 private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
61 new HashMap<>(4);
62
Jack Yu7afa1c02019-11-07 22:59:46 -080063 @VisibleForTesting
Jordan Liu997ab0a2019-11-14 14:23:49 -080064 public GsmCellBroadcastHandler(Context context, Looper looper) {
Jack Yu7afa1c02019-11-07 22:59:46 -080065 super("GsmCellBroadcastHandler", context, looper);
Jordan Liudfcbfaf2019-10-11 11:42:03 -070066 }
67
68 @Override
69 protected void onQuitting() {
70 super.onQuitting(); // release wakelock
71 }
72
73 /**
74 * Handle a GSM cell broadcast message passed from the telephony framework.
75 * @param message
76 */
77 public void onGsmCellBroadcastSms(int slotIndex, byte[] message) {
78 sendMessage(EVENT_NEW_SMS_MESSAGE, slotIndex, -1, message);
79 }
80
81 /**
82 * Create a new CellBroadcastHandler.
83 * @param context the context to use for dispatching Intents
84 * @return the new handler
85 */
86 public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context) {
Jack Yu7afa1c02019-11-07 22:59:46 -080087 GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, Looper.myLooper());
Jordan Liudfcbfaf2019-10-11 11:42:03 -070088 handler.start();
89 return handler;
90 }
91
92 /**
93 * Find the cell broadcast messages specify by the geo-fencing trigger message and perform a
94 * geo-fencing check for these messages.
95 * @param geoFencingTriggerMessage the trigger message
96 *
97 * @return {@code True} if geo-fencing is need for some cell broadcast message.
98 */
99 private boolean handleGeoFencingTriggerMessage(
100 GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex) {
101 final List<SmsCbMessage> cbMessages = new ArrayList<>();
102 final List<Uri> cbMessageUris = new ArrayList<>();
103
Jack Yu7afa1c02019-11-07 22:59:46 -0800104 SubscriptionManager subMgr = (SubscriptionManager) mContext.getSystemService(
105 Context.TELEPHONY_SUBSCRIPTION_SERVICE);
106 int[] subIds = subMgr.getSubscriptionIds(slotIndex);
107 Resources res;
108 if (subIds != null) {
109 res = getResources(subIds[0]);
110 } else {
111 res = getResources(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
112 }
113
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700114 // Only consider the cell broadcast received within 24 hours.
115 long lastReceivedTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS;
116
Jack Yu7afa1c02019-11-07 22:59:46 -0800117 // Some carriers require reset duplication detection after airplane mode or reboot.
118 if (res.getBoolean(R.bool.reset_on_power_cycle_or_airplane_mode)) {
119 lastReceivedTime = Long.max(lastReceivedTime, mLastAirplaneModeTime);
120 lastReceivedTime = Long.max(lastReceivedTime,
121 System.currentTimeMillis() - SystemClock.elapsedRealtime());
122 }
123
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700124 // Find the cell broadcast message identify by the message identifier and serial number
125 // and is not broadcasted.
126 String where = CellBroadcasts.SERVICE_CATEGORY + "=? AND "
127 + CellBroadcasts.SERIAL_NUMBER + "=? AND "
128 + CellBroadcasts.MESSAGE_BROADCASTED + "=? AND "
129 + CellBroadcasts.RECEIVED_TIME + ">?";
130
131 ContentResolver resolver = mContext.getContentResolver();
132 for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) {
Chen Xu34c71672019-10-21 22:56:37 -0700133 try (Cursor cursor = resolver.query(CellBroadcasts.CONTENT_URI,
Jordan Liub51f6932019-11-15 11:13:59 -0800134 CellBroadcastProvider.QUERY_COLUMNS,
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700135 where,
136 new String[] { Integer.toString(identity.messageIdentifier),
137 Integer.toString(identity.serialNumber), MESSAGE_NOT_BROADCASTED,
138 Long.toString(lastReceivedTime) },
139 null /* sortOrder */)) {
140 if (cursor != null) {
141 while (cursor.moveToNext()) {
142 cbMessages.add(SmsCbMessage.createFromCursor(cursor));
Chen Xu34c71672019-10-21 22:56:37 -0700143 cbMessageUris.add(ContentUris.withAppendedId(CellBroadcasts.CONTENT_URI,
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700144 cursor.getInt(cursor.getColumnIndex(CellBroadcasts._ID))));
145 }
146 }
147 }
148 }
149
Jack Yu7afa1c02019-11-07 22:59:46 -0800150 log("Found " + cbMessages.size() + " not broadcasted messages since "
151 + DateFormat.getDateTimeInstance().format(lastReceivedTime));
152
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700153 List<Geometry> commonBroadcastArea = new ArrayList<>();
154 if (geoFencingTriggerMessage.shouldShareBroadcastArea()) {
155 for (SmsCbMessage msg : cbMessages) {
156 if (msg.getGeometries() != null) {
157 commonBroadcastArea.addAll(msg.getGeometries());
158 }
159 }
160 }
161
162 // ATIS doesn't specify the geo fencing maximum wait time for the cell broadcasts specified
163 // in geo fencing trigger message. We will pick the largest maximum wait time among these
164 // cell broadcasts.
165 int maximumWaitTimeSec = 0;
166 for (SmsCbMessage msg : cbMessages) {
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700167 maximumWaitTimeSec = Math.max(maximumWaitTimeSec, msg.getMaximumWaitingDuration());
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700168 }
169
170 if (DBG) {
171 logd("Geo-fencing trigger message = " + geoFencingTriggerMessage);
172 for (SmsCbMessage msg : cbMessages) {
173 logd(msg.toString());
174 }
175 }
176
177 if (cbMessages.isEmpty()) {
178 if (DBG) logd("No CellBroadcast message need to be broadcasted");
179 return false;
180 }
181
182 requestLocationUpdate(location -> {
183 if (location == null) {
184 // If the location is not available, broadcast the messages directly.
Jack Yu8cb5d152019-10-23 11:35:32 -0700185 for (int i = 0; i < cbMessages.size(); i++) {
186 broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
187 }
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700188 } else {
189 for (int i = 0; i < cbMessages.size(); i++) {
190 List<Geometry> broadcastArea = !commonBroadcastArea.isEmpty()
191 ? commonBroadcastArea : cbMessages.get(i).getGeometries();
192 if (broadcastArea == null || broadcastArea.isEmpty()) {
193 broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
194 } else {
195 performGeoFencing(cbMessages.get(i), cbMessageUris.get(i), broadcastArea,
196 location, slotIndex);
197 }
198 }
199 }
200 }, maximumWaitTimeSec);
201 return true;
202 }
203
204 /**
205 * Handle 3GPP-format Cell Broadcast messages sent from radio.
206 *
207 * @param message the message to process
208 * @return true if need to wait for geo-fencing or an ordered broadcast was sent.
209 */
210 @Override
211 protected boolean handleSmsMessage(Message message) {
212 // For GSM, message.obj should be a byte[]
213 int slotIndex = message.arg1;
214 if (message.obj instanceof byte[]) {
215 byte[] pdu = (byte[]) message.obj;
216 SmsCbHeader header = createSmsCbHeader(pdu);
217 if (header == null) return false;
218
Jack Yu7afa1c02019-11-07 22:59:46 -0800219 log("header=" + header);
Jordan Liu5e174552019-11-07 11:54:10 -0800220 if (header.getServiceCategory() == SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) {
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700221 GeoFencingTriggerMessage triggerMessage =
222 GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu);
223 if (triggerMessage != null) {
224 return handleGeoFencingTriggerMessage(triggerMessage, slotIndex);
225 }
226 } else {
227 SmsCbMessage cbMessage = handleGsmBroadcastSms(header, pdu, slotIndex);
228 if (cbMessage != null) {
Jack Yu2390cad2019-10-24 00:19:49 -0700229 if (isDuplicate(cbMessage)) {
230 return false;
231 }
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700232 handleBroadcastSms(cbMessage);
233 return true;
234 }
235 if (VDBG) log("Not handled GSM broadcasts.");
236 }
237 }
238 return super.handleSmsMessage(message);
239 }
240
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700241 // return the GSM cell location from the first GSM cell info
242 private Pair<Integer, Integer> getGsmLacAndCid() {
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700243 TelephonyManager tm =
244 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
245 List<CellInfo> infos = tm.getAllCellInfo();
246 for (CellInfo info : infos) {
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700247 CellIdentity ci = info.getCellIdentity();
248 if (ci instanceof CellIdentityGsm) {
249 CellIdentityGsm ciGsm = (CellIdentityGsm) ci;
250 int lac = ciGsm.getLac() != CellInfo.UNAVAILABLE ? ciGsm.getLac() : -1;
251 int cid = ciGsm.getCid() != CellInfo.UNAVAILABLE ? ciGsm.getCid() : -1;
252 return Pair.create(lac, cid);
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700253 }
254 }
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700255 return null;
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700256 }
257
258
259 /**
260 * Handle 3GPP format SMS-CB message.
261 * @param header the cellbroadcast header.
262 * @param receivedPdu the received PDUs as a byte[]
263 */
264 private SmsCbMessage handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu,
265 int slotIndex) {
266 try {
267 if (VDBG) {
268 int pduLength = receivedPdu.length;
269 for (int i = 0; i < pduLength; i += 8) {
270 StringBuilder sb = new StringBuilder("SMS CB pdu data: ");
271 for (int j = i; j < i + 8 && j < pduLength; j++) {
272 int b = receivedPdu[j] & 0xff;
273 if (b < 0x10) {
274 sb.append('0');
275 }
276 sb.append(Integer.toHexString(b)).append(' ');
277 }
278 log(sb.toString());
279 }
280 }
281
282 if (VDBG) log("header=" + header);
283 TelephonyManager tm =
284 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
Jordan Liuac12bae2019-11-21 11:59:07 -0800285 tm.createForSubscriptionId(getSubIdForPhone(mContext, slotIndex));
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700286 // TODO make a systemAPI for getNetworkOperatorForSlotIndex
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700287 String plmn = tm.getSimOperator();
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700288 int lac = -1;
289 int cid = -1;
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700290 Pair<Integer, Integer> lacAndCid = getGsmLacAndCid();
291 // Check if GSM lac and cid are available. This is required to support
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700292 // dual-mode devices such as CDMA/LTE devices that require support for
293 // both 3GPP and 3GPP2 format messages
Jordan Liu8d7d0a12019-10-21 12:30:56 -0700294 if (lacAndCid != null) {
295 lac = lacAndCid.first;
296 cid = lacAndCid.second;
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700297 }
298
299 SmsCbLocation location;
300 switch (header.getGeographicalScope()) {
301 case SmsCbMessage.GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE:
302 location = new SmsCbLocation(plmn, lac, -1);
303 break;
304
305 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
306 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
307 location = new SmsCbLocation(plmn, lac, cid);
308 break;
309
310 case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
311 default:
Jordan Liuac12bae2019-11-21 11:59:07 -0800312 location = new SmsCbLocation(plmn, -1, -1);
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700313 break;
314 }
315
316 byte[][] pdus;
317 int pageCount = header.getNumberOfPages();
318 if (pageCount > 1) {
319 // Multi-page message
320 SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location);
321
322 // Try to find other pages of the same message
323 pdus = mSmsCbPageMap.get(concatInfo);
324
325 if (pdus == null) {
326 // This is the first page of this message, make room for all
327 // pages and keep until complete
328 pdus = new byte[pageCount][];
329
330 mSmsCbPageMap.put(concatInfo, pdus);
331 }
332
333 if (VDBG) log("pdus size=" + pdus.length);
334 // Page parameter is one-based
335 pdus[header.getPageIndex() - 1] = receivedPdu;
336
337 for (byte[] pdu : pdus) {
338 if (pdu == null) {
339 // Still missing pages, exit
340 log("still missing pdu");
341 return null;
342 }
343 }
344
345 // Message complete, remove and dispatch
346 mSmsCbPageMap.remove(concatInfo);
347 } else {
348 // Single page message
349 pdus = new byte[1][];
350 pdus[0] = receivedPdu;
351 }
352
353 // Remove messages that are out of scope to prevent the map from
354 // growing indefinitely, containing incomplete messages that were
355 // never assembled
356 Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator();
357
358 while (iter.hasNext()) {
359 SmsCbConcatInfo info = iter.next();
360
361 if (!info.matchesLocation(plmn, lac, cid)) {
362 iter.remove();
363 }
364 }
365
366 return GsmSmsCbMessage.createSmsCbMessage(mContext, header, location, pdus, slotIndex);
367
368 } catch (RuntimeException e) {
369 loge("Error in decoding SMS CB pdu", e);
370 return null;
371 }
372 }
373
374 private SmsCbHeader createSmsCbHeader(byte[] bytes) {
375 try {
376 return new SmsCbHeader(bytes);
377 } catch (Exception ex) {
378 loge("Can't create SmsCbHeader, ex = " + ex.toString());
379 return null;
380 }
381 }
382
383 /**
384 * Holds all info about a message page needed to assemble a complete concatenated message.
385 */
386 private static final class SmsCbConcatInfo {
387
388 private final SmsCbHeader mHeader;
389 private final SmsCbLocation mLocation;
390
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700391 SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) {
392 mHeader = header;
393 mLocation = location;
394 }
395
396 @Override
397 public int hashCode() {
398 return (mHeader.getSerialNumber() * 31) + mLocation.hashCode();
399 }
400
401 @Override
402 public boolean equals(Object obj) {
403 if (obj instanceof SmsCbConcatInfo) {
404 SmsCbConcatInfo other = (SmsCbConcatInfo) obj;
405
406 // Two pages match if they have the same serial number (which includes the
407 // geographical scope and update number), and both pages belong to the same
408 // location (PLMN, plus LAC and CID if these are part of the geographical scope).
409 return mHeader.getSerialNumber() == other.mHeader.getSerialNumber()
410 && mLocation.equals(other.mLocation);
411 }
412
413 return false;
414 }
415
416 /**
417 * Compare the location code for this message to the current location code. The match is
418 * relative to the geographical scope of the message, which determines whether the LAC
419 * and Cell ID are saved in mLocation or set to -1 to match all values.
420 *
421 * @param plmn the current PLMN
422 * @param lac the current Location Area (GSM) or Service Area (UMTS)
423 * @param cid the current Cell ID
424 * @return true if this message is valid for the current location; false otherwise
425 */
Jordan Liudfcbfaf2019-10-11 11:42:03 -0700426 public boolean matchesLocation(String plmn, int lac, int cid) {
427 return mLocation.isInLocationArea(plmn, lac, cid);
428 }
429 }
430}