blob: a61cc6d5865c8bab7e9798b9d230ad809ed0e714 [file] [log] [blame]
Jordan Liuc872fad2019-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
Jack Yu87db6b72020-01-24 11:45:59 -080019import android.annotation.NonNull;
Jordan Liuc872fad2019-10-11 11:42:03 -070020import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.Context;
Jack Yu87db6b72020-01-24 11:45:59 -080023import android.content.Intent;
Jack Yue7591d32019-11-07 22:59:46 -080024import android.content.res.Resources;
Jordan Liuc872fad2019-10-11 11:42:03 -070025import android.database.Cursor;
26import android.net.Uri;
Jack Yue7591d32019-11-07 22:59:46 -080027import android.os.Looper;
Jordan Liuc872fad2019-10-11 11:42:03 -070028import android.os.Message;
Jack Yue7591d32019-11-07 22:59:46 -080029import android.os.SystemClock;
Jack Yu87db6b72020-01-24 11:45:59 -080030import android.os.UserHandle;
Jordan Liuc872fad2019-10-11 11:42:03 -070031import android.provider.Telephony.CellBroadcasts;
Jordan Liu4a2ff262019-10-21 12:30:56 -070032import android.telephony.CbGeoUtils.Geometry;
Jack Yu87db6b72020-01-24 11:45:59 -080033import android.telephony.CellBroadcastIntents;
Jordan Liu4a2ff262019-10-21 12:30:56 -070034import android.telephony.CellIdentity;
35import android.telephony.CellIdentityGsm;
Jordan Liuc872fad2019-10-11 11:42:03 -070036import android.telephony.CellInfo;
Jordan Liuc872fad2019-10-11 11:42:03 -070037import android.telephony.SmsCbLocation;
38import android.telephony.SmsCbMessage;
Jack Yue7591d32019-11-07 22:59:46 -080039import android.telephony.SubscriptionManager;
Jordan Liuc872fad2019-10-11 11:42:03 -070040import android.telephony.TelephonyManager;
Jordan Liuc872fad2019-10-11 11:42:03 -070041import android.text.format.DateUtils;
Jordan Liu4a2ff262019-10-21 12:30:56 -070042import android.util.Pair;
Jack Yu87db6b72020-01-24 11:45:59 -080043import android.util.SparseArray;
Jordan Liuc872fad2019-10-11 11:42:03 -070044
45import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage;
46import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
Jack Yue7591d32019-11-07 22:59:46 -080047import com.android.internal.annotations.VisibleForTesting;
Jordan Liuc872fad2019-10-11 11:42:03 -070048
Jack Yue7591d32019-11-07 22:59:46 -080049import java.text.DateFormat;
Jordan Liuc872fad2019-10-11 11:42:03 -070050import java.util.ArrayList;
51import java.util.HashMap;
52import java.util.Iterator;
53import java.util.List;
Jack Yu87db6b72020-01-24 11:45:59 -080054import java.util.stream.IntStream;
Jordan Liuc872fad2019-10-11 11:42:03 -070055
56/**
57 * Handler for 3GPP format Cell Broadcasts. Parent class can also handle CDMA Cell Broadcasts.
58 */
59public class GsmCellBroadcastHandler extends CellBroadcastHandler {
60 private static final boolean VDBG = false; // log CB PDU data
61
62 /** Indicates that a message is not being broadcasted. */
63 private static final String MESSAGE_NOT_BROADCASTED = "0";
64
Jack Yu87db6b72020-01-24 11:45:59 -080065 private SparseArray<String> mAreaInfos = new SparseArray<>();
66
Jordan Liuc872fad2019-10-11 11:42:03 -070067 /** This map holds incomplete concatenated messages waiting for assembly. */
Jordan Liuc872fad2019-10-11 11:42:03 -070068 private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
69 new HashMap<>(4);
70
Jack Yue7591d32019-11-07 22:59:46 -080071 @VisibleForTesting
Jordan Liuf24590f2019-11-14 14:23:49 -080072 public GsmCellBroadcastHandler(Context context, Looper looper) {
Jack Yue7591d32019-11-07 22:59:46 -080073 super("GsmCellBroadcastHandler", context, looper);
Jordan Liuc872fad2019-10-11 11:42:03 -070074 }
75
76 @Override
77 protected void onQuitting() {
78 super.onQuitting(); // release wakelock
79 }
80
81 /**
82 * Handle a GSM cell broadcast message passed from the telephony framework.
83 * @param message
84 */
85 public void onGsmCellBroadcastSms(int slotIndex, byte[] message) {
86 sendMessage(EVENT_NEW_SMS_MESSAGE, slotIndex, -1, message);
87 }
88
89 /**
Jack Yu87db6b72020-01-24 11:45:59 -080090 * Get the area information
91 *
92 * @param slotIndex SIM slot index
93 * @return The area information
94 */
95 @NonNull
96 public String getCellBroadcastAreaInfo(int slotIndex) {
97 String info = mAreaInfos.get(slotIndex);
98 return info == null ? "" : info;
99 }
100
101 /**
Jordan Liuc872fad2019-10-11 11:42:03 -0700102 * Create a new CellBroadcastHandler.
103 * @param context the context to use for dispatching Intents
104 * @return the new handler
105 */
106 public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context) {
Jack Yue7591d32019-11-07 22:59:46 -0800107 GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, Looper.myLooper());
Jordan Liuc872fad2019-10-11 11:42:03 -0700108 handler.start();
109 return handler;
110 }
111
112 /**
113 * Find the cell broadcast messages specify by the geo-fencing trigger message and perform a
114 * geo-fencing check for these messages.
115 * @param geoFencingTriggerMessage the trigger message
116 *
117 * @return {@code True} if geo-fencing is need for some cell broadcast message.
118 */
119 private boolean handleGeoFencingTriggerMessage(
120 GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex) {
121 final List<SmsCbMessage> cbMessages = new ArrayList<>();
122 final List<Uri> cbMessageUris = new ArrayList<>();
123
Jack Yue7591d32019-11-07 22:59:46 -0800124 SubscriptionManager subMgr = (SubscriptionManager) mContext.getSystemService(
125 Context.TELEPHONY_SUBSCRIPTION_SERVICE);
126 int[] subIds = subMgr.getSubscriptionIds(slotIndex);
127 Resources res;
128 if (subIds != null) {
129 res = getResources(subIds[0]);
130 } else {
131 res = getResources(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
132 }
133
Jordan Liuc872fad2019-10-11 11:42:03 -0700134 // Only consider the cell broadcast received within 24 hours.
135 long lastReceivedTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS;
136
Jack Yue7591d32019-11-07 22:59:46 -0800137 // Some carriers require reset duplication detection after airplane mode or reboot.
138 if (res.getBoolean(R.bool.reset_on_power_cycle_or_airplane_mode)) {
139 lastReceivedTime = Long.max(lastReceivedTime, mLastAirplaneModeTime);
140 lastReceivedTime = Long.max(lastReceivedTime,
141 System.currentTimeMillis() - SystemClock.elapsedRealtime());
142 }
143
Jordan Liuc872fad2019-10-11 11:42:03 -0700144 // Find the cell broadcast message identify by the message identifier and serial number
145 // and is not broadcasted.
146 String where = CellBroadcasts.SERVICE_CATEGORY + "=? AND "
147 + CellBroadcasts.SERIAL_NUMBER + "=? AND "
148 + CellBroadcasts.MESSAGE_BROADCASTED + "=? AND "
149 + CellBroadcasts.RECEIVED_TIME + ">?";
150
151 ContentResolver resolver = mContext.getContentResolver();
152 for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) {
Chen Xu93fdfeb2019-10-21 22:56:37 -0700153 try (Cursor cursor = resolver.query(CellBroadcasts.CONTENT_URI,
Jordan Liud3eeb402019-11-15 11:13:59 -0800154 CellBroadcastProvider.QUERY_COLUMNS,
Jordan Liuc872fad2019-10-11 11:42:03 -0700155 where,
156 new String[] { Integer.toString(identity.messageIdentifier),
157 Integer.toString(identity.serialNumber), MESSAGE_NOT_BROADCASTED,
158 Long.toString(lastReceivedTime) },
159 null /* sortOrder */)) {
160 if (cursor != null) {
161 while (cursor.moveToNext()) {
162 cbMessages.add(SmsCbMessage.createFromCursor(cursor));
Chen Xu93fdfeb2019-10-21 22:56:37 -0700163 cbMessageUris.add(ContentUris.withAppendedId(CellBroadcasts.CONTENT_URI,
Jordan Liuc872fad2019-10-11 11:42:03 -0700164 cursor.getInt(cursor.getColumnIndex(CellBroadcasts._ID))));
165 }
166 }
167 }
168 }
169
Jack Yue7591d32019-11-07 22:59:46 -0800170 log("Found " + cbMessages.size() + " not broadcasted messages since "
171 + DateFormat.getDateTimeInstance().format(lastReceivedTime));
172
Jordan Liuc872fad2019-10-11 11:42:03 -0700173 List<Geometry> commonBroadcastArea = new ArrayList<>();
174 if (geoFencingTriggerMessage.shouldShareBroadcastArea()) {
175 for (SmsCbMessage msg : cbMessages) {
176 if (msg.getGeometries() != null) {
177 commonBroadcastArea.addAll(msg.getGeometries());
178 }
179 }
180 }
181
182 // ATIS doesn't specify the geo fencing maximum wait time for the cell broadcasts specified
183 // in geo fencing trigger message. We will pick the largest maximum wait time among these
184 // cell broadcasts.
185 int maximumWaitTimeSec = 0;
186 for (SmsCbMessage msg : cbMessages) {
Jordan Liu4a2ff262019-10-21 12:30:56 -0700187 maximumWaitTimeSec = Math.max(maximumWaitTimeSec, msg.getMaximumWaitingDuration());
Jordan Liuc872fad2019-10-11 11:42:03 -0700188 }
189
190 if (DBG) {
191 logd("Geo-fencing trigger message = " + geoFencingTriggerMessage);
192 for (SmsCbMessage msg : cbMessages) {
193 logd(msg.toString());
194 }
195 }
196
197 if (cbMessages.isEmpty()) {
198 if (DBG) logd("No CellBroadcast message need to be broadcasted");
199 return false;
200 }
201
202 requestLocationUpdate(location -> {
203 if (location == null) {
204 // If the location is not available, broadcast the messages directly.
Jack Yu6d223a92019-10-23 11:35:32 -0700205 for (int i = 0; i < cbMessages.size(); i++) {
206 broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
207 }
Jordan Liuc872fad2019-10-11 11:42:03 -0700208 } else {
209 for (int i = 0; i < cbMessages.size(); i++) {
210 List<Geometry> broadcastArea = !commonBroadcastArea.isEmpty()
211 ? commonBroadcastArea : cbMessages.get(i).getGeometries();
212 if (broadcastArea == null || broadcastArea.isEmpty()) {
213 broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
214 } else {
215 performGeoFencing(cbMessages.get(i), cbMessageUris.get(i), broadcastArea,
216 location, slotIndex);
217 }
218 }
219 }
220 }, maximumWaitTimeSec);
221 return true;
222 }
223
224 /**
Jack Yu87db6b72020-01-24 11:45:59 -0800225 * Process area info message.
226 *
227 * @param slotIndex SIM slot index
228 * @param message Cell broadcast message
229 * @return {@code true} if the mssage is an area info message and got processed correctly,
230 * otherwise {@code false}.
231 */
232 private boolean handleAreaInfoMessage(int slotIndex, SmsCbMessage message) {
233 SubscriptionManager subMgr = (SubscriptionManager) mContext.getSystemService(
234 Context.TELEPHONY_SUBSCRIPTION_SERVICE);
235
236 // Check area info message
237 int[] subIds = subMgr.getSubscriptionIds(slotIndex);
238 Resources res;
239 if (subIds != null) {
240 res = getResources(subIds[0]);
241 } else {
242 res = getResources(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
243 }
244 int[] areaInfoChannels = res.getIntArray(R.array.area_info_channels);
245
246 if (IntStream.of(areaInfoChannels).anyMatch(
247 x -> x == message.getServiceCategory())) {
248 mAreaInfos.put(slotIndex, message.getMessageBody());
249
250 String[] pkgs = mContext.getResources().getStringArray(
251 R.array.config_area_info_receiver_packages);
252 for (String pkg : pkgs) {
253 Intent intent = new Intent(CellBroadcastIntents.ACTION_AREA_INFO_UPDATED);
254 intent.setPackage(pkg);
255 mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
256 android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
257 }
258 return true;
259 }
260
261 // This is not an area info message.
262 return false;
263 }
264
265 /**
Jordan Liuc872fad2019-10-11 11:42:03 -0700266 * Handle 3GPP-format Cell Broadcast messages sent from radio.
267 *
268 * @param message the message to process
269 * @return true if need to wait for geo-fencing or an ordered broadcast was sent.
270 */
271 @Override
272 protected boolean handleSmsMessage(Message message) {
273 // For GSM, message.obj should be a byte[]
274 int slotIndex = message.arg1;
275 if (message.obj instanceof byte[]) {
276 byte[] pdu = (byte[]) message.obj;
277 SmsCbHeader header = createSmsCbHeader(pdu);
278 if (header == null) return false;
279
Jack Yue7591d32019-11-07 22:59:46 -0800280 log("header=" + header);
Jordan Liube4fe782019-11-07 11:54:10 -0800281 if (header.getServiceCategory() == SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) {
Jordan Liuc872fad2019-10-11 11:42:03 -0700282 GeoFencingTriggerMessage triggerMessage =
283 GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu);
284 if (triggerMessage != null) {
285 return handleGeoFencingTriggerMessage(triggerMessage, slotIndex);
286 }
287 } else {
288 SmsCbMessage cbMessage = handleGsmBroadcastSms(header, pdu, slotIndex);
289 if (cbMessage != null) {
Jack Yua1309272019-10-24 00:19:49 -0700290 if (isDuplicate(cbMessage)) {
291 return false;
292 }
Jack Yu87db6b72020-01-24 11:45:59 -0800293
294 if (handleAreaInfoMessage(slotIndex, cbMessage)) {
295 log("Channel " + cbMessage.getServiceCategory() + " message processed");
296 return false;
297 }
298
Jordan Liuc872fad2019-10-11 11:42:03 -0700299 handleBroadcastSms(cbMessage);
300 return true;
301 }
302 if (VDBG) log("Not handled GSM broadcasts.");
303 }
304 }
305 return super.handleSmsMessage(message);
306 }
307
Jordan Liu4a2ff262019-10-21 12:30:56 -0700308 // return the GSM cell location from the first GSM cell info
309 private Pair<Integer, Integer> getGsmLacAndCid() {
Jordan Liuc872fad2019-10-11 11:42:03 -0700310 TelephonyManager tm =
311 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
312 List<CellInfo> infos = tm.getAllCellInfo();
313 for (CellInfo info : infos) {
Jordan Liu4a2ff262019-10-21 12:30:56 -0700314 CellIdentity ci = info.getCellIdentity();
315 if (ci instanceof CellIdentityGsm) {
316 CellIdentityGsm ciGsm = (CellIdentityGsm) ci;
317 int lac = ciGsm.getLac() != CellInfo.UNAVAILABLE ? ciGsm.getLac() : -1;
318 int cid = ciGsm.getCid() != CellInfo.UNAVAILABLE ? ciGsm.getCid() : -1;
319 return Pair.create(lac, cid);
Jordan Liuc872fad2019-10-11 11:42:03 -0700320 }
321 }
Jordan Liu4a2ff262019-10-21 12:30:56 -0700322 return null;
Jordan Liuc872fad2019-10-11 11:42:03 -0700323 }
324
325
326 /**
327 * Handle 3GPP format SMS-CB message.
328 * @param header the cellbroadcast header.
329 * @param receivedPdu the received PDUs as a byte[]
330 */
331 private SmsCbMessage handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu,
332 int slotIndex) {
333 try {
334 if (VDBG) {
335 int pduLength = receivedPdu.length;
336 for (int i = 0; i < pduLength; i += 8) {
337 StringBuilder sb = new StringBuilder("SMS CB pdu data: ");
338 for (int j = i; j < i + 8 && j < pduLength; j++) {
339 int b = receivedPdu[j] & 0xff;
340 if (b < 0x10) {
341 sb.append('0');
342 }
343 sb.append(Integer.toHexString(b)).append(' ');
344 }
345 log(sb.toString());
346 }
347 }
348
349 if (VDBG) log("header=" + header);
350 TelephonyManager tm =
351 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
Jordan Liucdfc9f32019-11-21 11:59:07 -0800352 tm.createForSubscriptionId(getSubIdForPhone(mContext, slotIndex));
Jordan Liuc872fad2019-10-11 11:42:03 -0700353 // TODO make a systemAPI for getNetworkOperatorForSlotIndex
Jordan Liu4a2ff262019-10-21 12:30:56 -0700354 String plmn = tm.getSimOperator();
Jordan Liuc872fad2019-10-11 11:42:03 -0700355 int lac = -1;
356 int cid = -1;
Jordan Liu4a2ff262019-10-21 12:30:56 -0700357 Pair<Integer, Integer> lacAndCid = getGsmLacAndCid();
358 // Check if GSM lac and cid are available. This is required to support
Jordan Liuc872fad2019-10-11 11:42:03 -0700359 // dual-mode devices such as CDMA/LTE devices that require support for
360 // both 3GPP and 3GPP2 format messages
Jordan Liu4a2ff262019-10-21 12:30:56 -0700361 if (lacAndCid != null) {
362 lac = lacAndCid.first;
363 cid = lacAndCid.second;
Jordan Liuc872fad2019-10-11 11:42:03 -0700364 }
365
366 SmsCbLocation location;
367 switch (header.getGeographicalScope()) {
368 case SmsCbMessage.GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE:
369 location = new SmsCbLocation(plmn, lac, -1);
370 break;
371
372 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
373 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
374 location = new SmsCbLocation(plmn, lac, cid);
375 break;
376
377 case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
378 default:
Jordan Liucdfc9f32019-11-21 11:59:07 -0800379 location = new SmsCbLocation(plmn, -1, -1);
Jordan Liuc872fad2019-10-11 11:42:03 -0700380 break;
381 }
382
383 byte[][] pdus;
384 int pageCount = header.getNumberOfPages();
385 if (pageCount > 1) {
386 // Multi-page message
387 SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location);
388
389 // Try to find other pages of the same message
390 pdus = mSmsCbPageMap.get(concatInfo);
391
392 if (pdus == null) {
393 // This is the first page of this message, make room for all
394 // pages and keep until complete
395 pdus = new byte[pageCount][];
396
397 mSmsCbPageMap.put(concatInfo, pdus);
398 }
399
400 if (VDBG) log("pdus size=" + pdus.length);
401 // Page parameter is one-based
402 pdus[header.getPageIndex() - 1] = receivedPdu;
403
404 for (byte[] pdu : pdus) {
405 if (pdu == null) {
406 // Still missing pages, exit
407 log("still missing pdu");
408 return null;
409 }
410 }
411
412 // Message complete, remove and dispatch
413 mSmsCbPageMap.remove(concatInfo);
414 } else {
415 // Single page message
416 pdus = new byte[1][];
417 pdus[0] = receivedPdu;
418 }
419
420 // Remove messages that are out of scope to prevent the map from
421 // growing indefinitely, containing incomplete messages that were
422 // never assembled
423 Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator();
424
425 while (iter.hasNext()) {
426 SmsCbConcatInfo info = iter.next();
427
428 if (!info.matchesLocation(plmn, lac, cid)) {
429 iter.remove();
430 }
431 }
432
433 return GsmSmsCbMessage.createSmsCbMessage(mContext, header, location, pdus, slotIndex);
434
435 } catch (RuntimeException e) {
436 loge("Error in decoding SMS CB pdu", e);
437 return null;
438 }
439 }
440
441 private SmsCbHeader createSmsCbHeader(byte[] bytes) {
442 try {
443 return new SmsCbHeader(bytes);
444 } catch (Exception ex) {
445 loge("Can't create SmsCbHeader, ex = " + ex.toString());
446 return null;
447 }
448 }
449
450 /**
451 * Holds all info about a message page needed to assemble a complete concatenated message.
452 */
453 private static final class SmsCbConcatInfo {
454
455 private final SmsCbHeader mHeader;
456 private final SmsCbLocation mLocation;
457
Jordan Liuc872fad2019-10-11 11:42:03 -0700458 SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) {
459 mHeader = header;
460 mLocation = location;
461 }
462
463 @Override
464 public int hashCode() {
465 return (mHeader.getSerialNumber() * 31) + mLocation.hashCode();
466 }
467
468 @Override
469 public boolean equals(Object obj) {
470 if (obj instanceof SmsCbConcatInfo) {
471 SmsCbConcatInfo other = (SmsCbConcatInfo) obj;
472
473 // Two pages match if they have the same serial number (which includes the
474 // geographical scope and update number), and both pages belong to the same
475 // location (PLMN, plus LAC and CID if these are part of the geographical scope).
476 return mHeader.getSerialNumber() == other.mHeader.getSerialNumber()
477 && mLocation.equals(other.mLocation);
478 }
479
480 return false;
481 }
482
483 /**
484 * Compare the location code for this message to the current location code. The match is
485 * relative to the geographical scope of the message, which determines whether the LAC
486 * and Cell ID are saved in mLocation or set to -1 to match all values.
487 *
488 * @param plmn the current PLMN
489 * @param lac the current Location Area (GSM) or Service Area (UMTS)
490 * @param cid the current Cell ID
491 * @return true if this message is valid for the current location; false otherwise
492 */
Jordan Liuc872fad2019-10-11 11:42:03 -0700493 public boolean matchesLocation(String plmn, int lac, int cid) {
494 return mLocation.isInLocationArea(plmn, lac, cid);
495 }
496 }
497}