• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.cellbroadcastservice;
18 
19 import static com.android.cellbroadcastservice.CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__GSM_INVALID_PDU;
20 import static com.android.cellbroadcastservice.CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__UNEXPECTED_GSM_MESSAGE_TYPE_FROM_FWK;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.content.BroadcastReceiver;
25 import android.content.ContentResolver;
26 import android.content.ContentUris;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.content.res.Resources;
31 import android.database.Cursor;
32 import android.net.Uri;
33 import android.os.Looper;
34 import android.os.Message;
35 import android.os.SystemClock;
36 import android.os.UserHandle;
37 import android.provider.Telephony.CellBroadcasts;
38 import android.telephony.AccessNetworkConstants;
39 import android.telephony.CarrierConfigManager;
40 import android.telephony.CbGeoUtils;
41 import android.telephony.CbGeoUtils.Geometry;
42 import android.telephony.CellBroadcastIntents;
43 import android.telephony.CellIdentity;
44 import android.telephony.CellIdentityGsm;
45 import android.telephony.CellIdentityLte;
46 import android.telephony.CellIdentityNr;
47 import android.telephony.CellIdentityTdscdma;
48 import android.telephony.CellIdentityWcdma;
49 import android.telephony.CellInfo;
50 import android.telephony.NetworkRegistrationInfo;
51 import android.telephony.PhoneStateListener;
52 import android.telephony.ServiceState;
53 import android.telephony.SmsCbLocation;
54 import android.telephony.SmsCbMessage;
55 import android.telephony.SubscriptionInfo;
56 import android.telephony.SubscriptionManager;
57 import android.telephony.TelephonyManager;
58 import android.text.TextUtils;
59 import android.text.format.DateUtils;
60 import android.util.Pair;
61 import android.util.SparseArray;
62 
63 import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage;
64 import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
65 import com.android.internal.annotations.VisibleForTesting;
66 
67 import java.text.DateFormat;
68 import java.util.ArrayList;
69 import java.util.Arrays;
70 import java.util.HashMap;
71 import java.util.Iterator;
72 import java.util.List;
73 import java.util.Objects;
74 import java.util.stream.Collectors;
75 import java.util.stream.IntStream;
76 
77 /**
78  * Handler for 3GPP format Cell Broadcasts. Parent class can also handle CDMA Cell Broadcasts.
79  */
80 public class GsmCellBroadcastHandler extends CellBroadcastHandler {
81     private static final boolean VDBG = false;  // log CB PDU data
82 
83     /** Indicates that a message is not displayed. */
84     private static final String MESSAGE_NOT_DISPLAYED = "0";
85 
86     /**
87      * Intent sent from cellbroadcastreceiver to notify cellbroadcastservice that area info update
88      * is disabled/enabled.
89      */
90     private static final String ACTION_AREA_UPDATE_ENABLED =
91             "com.android.cellbroadcastreceiver.action.AREA_UPDATE_INFO_ENABLED";
92 
93     /**
94      * The extra for cell ACTION_AREA_UPDATE_ENABLED enable/disable
95      */
96     private static final String EXTRA_ENABLE = "enable";
97 
98     /**
99      * This permission is only granted to the cellbroadcast mainline module and thus can be
100      * used for permission check within CBR and CBS.
101      */
102     private static final String CBR_MODULE_PERMISSION =
103             "com.android.cellbroadcastservice.FULL_ACCESS_CELL_BROADCAST_HISTORY";
104 
105     private final SparseArray<String> mAreaInfos = new SparseArray<>();
106 
107     /**
108      * Used to store ServiceStateListeners for each active slot
109      */
110     private final SparseArray<ServiceStateListener> mServiceStateListener = new SparseArray<>();
111 
112     /** This map holds incomplete concatenated messages waiting for assembly. */
113     private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
114             new HashMap<>(4);
115 
116     @VisibleForTesting
GsmCellBroadcastHandler(Context context, Looper looper, CbSendMessageCalculatorFactory cbSendMessageCalculatorFactory, CellBroadcastHandler.HandlerHelper handlerHelper)117     public GsmCellBroadcastHandler(Context context, Looper looper,
118             CbSendMessageCalculatorFactory cbSendMessageCalculatorFactory,
119             CellBroadcastHandler.HandlerHelper handlerHelper) {
120         super("GsmCellBroadcastHandler", context, looper, cbSendMessageCalculatorFactory,
121                 handlerHelper);
122         mContext.registerReceiver(mGsmReceiver, new IntentFilter(ACTION_AREA_UPDATE_ENABLED),
123                 CBR_MODULE_PERMISSION, null);
124         // Some OEMs want us to reset the area info updates when going out of service
125         // (Okay to use res for slot 0 here because this should not be carrier specific)
126         if (getResourcesForSlot(0).getBoolean(R.bool.reset_area_info_on_oos)) {
127             mContext.registerReceiver(mGsmReceiver,
128                     new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED), null,
129                     null);
130         }
131     }
132 
133     /**
134      * Constructor used only for tests. This constructor allows the caller to pass in resources
135      * and a subId to be put into the resources cache before getResourcesForSlot called (this is
136      * needed for unit tests to prevent
137      */
138     @VisibleForTesting
GsmCellBroadcastHandler(Context context, Looper looper, CbSendMessageCalculatorFactory cbSendMessageCalculatorFactory, CellBroadcastHandler.HandlerHelper handlerHelper, Resources resources, int subId)139     public GsmCellBroadcastHandler(Context context, Looper looper,
140             CbSendMessageCalculatorFactory cbSendMessageCalculatorFactory,
141             CellBroadcastHandler.HandlerHelper handlerHelper, Resources resources, int subId) {
142         super("GsmCellBroadcastHandler", context, looper, cbSendMessageCalculatorFactory,
143                 handlerHelper);
144         mContext.registerReceiver(mGsmReceiver, new IntentFilter(ACTION_AREA_UPDATE_ENABLED),
145                 CBR_MODULE_PERMISSION, null);
146 
147         // set the resources cache here for unit tests
148         mResourcesCache.put(subId, resources);
149 
150         // Some OEMs want us to reset the area info updates when going out of service
151         // (Okay to use res for slot 0 here because this should not be carrier specific)
152         if (getResourcesForSlot(0).getBoolean(R.bool.reset_area_info_on_oos)) {
153             mContext.registerReceiver(mGsmReceiver,
154                     new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED), null,
155                     null);
156         }
157     }
158 
159     @Override
cleanup()160     public void cleanup() {
161         log("cleanup");
162         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
163         int size = mServiceStateListener.size();
164         for (int i = 0; i < size; i++) {
165             tm.listen(mServiceStateListener.valueAt(i), PhoneStateListener.LISTEN_NONE);
166         }
167         mContext.unregisterReceiver(mGsmReceiver);
168         super.cleanup();
169     }
170 
registerServiceStateListeners()171     private void registerServiceStateListeners() {
172         // register for all active slots
173         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
174         SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
175         for (int slotId = 0; slotId < tm.getActiveModemCount(); slotId++) {
176             SubscriptionInfo info = sm.getActiveSubscriptionInfoForSimSlotIndex(slotId);
177             if (info != null) {
178                 int subId = info.getSubscriptionId();
179                 if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
180                     mServiceStateListener.put(slotId, new ServiceStateListener(subId, slotId));
181                     tm.createForSubscriptionId(subId).listen(mServiceStateListener.get(slotId),
182                             PhoneStateListener.LISTEN_SERVICE_STATE);
183                 }
184             }
185         }
186     }
187 
188     private class ServiceStateListener extends PhoneStateListener {
189         // subId is not needed for clearing area info, only used for debugging purposes
190         private int mSubId;
191         private int mSlotId;
192 
ServiceStateListener(int subId, int slotId)193         ServiceStateListener(int subId, int slotId) {
194             mSubId = subId;
195             mSlotId = slotId;
196         }
197 
198         @Override
onServiceStateChanged(@onNull ServiceState serviceState)199         public void onServiceStateChanged(@NonNull ServiceState serviceState) {
200             int state = serviceState.getState();
201             if (state == ServiceState.STATE_POWER_OFF
202                     || state == ServiceState.STATE_OUT_OF_SERVICE) {
203                 synchronized (mAreaInfos) {
204                     if (mAreaInfos.contains(mSlotId)) {
205                         log("OOS mSubId=" + mSubId + " mSlotId=" + mSlotId
206                                 + ", clearing area infos");
207                         mAreaInfos.remove(mSlotId);
208                     }
209                 }
210             }
211         }
212     }
213 
214     @Override
onQuitting()215     protected void onQuitting() {
216         super.onQuitting();     // release wakelock
217     }
218 
219     /**
220      * Handle a GSM cell broadcast message passed from the telephony framework.
221      * @param message
222      */
onGsmCellBroadcastSms(int slotIndex, byte[] message)223     public void onGsmCellBroadcastSms(int slotIndex, byte[] message) {
224         sendMessage(EVENT_NEW_SMS_MESSAGE, slotIndex, -1, message);
225     }
226 
227     /**
228      * Get the area information
229      *
230      * @param slotIndex SIM slot index
231      * @return The area information
232      */
233     @NonNull
getCellBroadcastAreaInfo(int slotIndex)234     public String getCellBroadcastAreaInfo(int slotIndex) {
235         String info;
236         synchronized (mAreaInfos) {
237             info = mAreaInfos.get(slotIndex);
238         }
239         return info == null ? "" : info;
240     }
241 
242     /**
243      * Create a new CellBroadcastHandler.
244      * @param context the context to use for dispatching Intents
245      * @return the new handler
246      */
makeGsmCellBroadcastHandler(Context context)247     public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context) {
248         GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, Looper.myLooper(),
249                 new CbSendMessageCalculatorFactory(), null);
250         handler.start();
251         return handler;
252     }
253 
getResourcesForSlot(int slotIndex)254     private Resources getResourcesForSlot(int slotIndex) {
255         SubscriptionManager subMgr = mContext.getSystemService(SubscriptionManager.class);
256         int[] subIds = subMgr.getSubscriptionIds(slotIndex);
257         Resources res;
258         if (subIds != null) {
259             res = getResources(subIds[0]);
260         } else {
261             res = getResources(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
262         }
263         return res;
264     }
265 
266     /**
267      * Find the cell broadcast messages specify by the geo-fencing trigger message and perform a
268      * geo-fencing check for these messages.
269      * @param geoFencingTriggerMessage the trigger message
270      *
271      * @return {@code True} if geo-fencing is need for some cell broadcast message.
272      */
handleGeoFencingTriggerMessage( GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex)273     private boolean handleGeoFencingTriggerMessage(
274             GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex) {
275         final List<SmsCbMessage> cbMessages = new ArrayList<>();
276         final List<Uri> cbMessageUris = new ArrayList<>();
277 
278         Resources res = getResourcesForSlot(slotIndex);
279 
280         // Only consider the cell broadcast received within 24 hours.
281         long lastReceivedTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS;
282 
283         // Some carriers require reset duplication detection after airplane mode or reboot.
284         if (res.getBoolean(R.bool.reset_on_power_cycle_or_airplane_mode)) {
285             lastReceivedTime = Long.max(lastReceivedTime, mLastAirplaneModeTime);
286             lastReceivedTime = Long.max(lastReceivedTime,
287                     System.currentTimeMillis() - SystemClock.elapsedRealtime());
288         }
289 
290         // Find the cell broadcast message identify by the message identifier and serial number
291         // and was not displayed.
292         String where = CellBroadcasts.SERVICE_CATEGORY + "=? AND "
293                 + CellBroadcasts.SERIAL_NUMBER + "=? AND "
294                 + CellBroadcasts.MESSAGE_DISPLAYED + "=? AND "
295                 + CellBroadcasts.RECEIVED_TIME + ">?";
296 
297         ContentResolver resolver = mContext.getContentResolver();
298         for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) {
299             try (Cursor cursor = resolver.query(CellBroadcasts.CONTENT_URI,
300                     CellBroadcastProvider.QUERY_COLUMNS,
301                     where,
302                     new String[] { Integer.toString(identity.messageIdentifier),
303                             Integer.toString(identity.serialNumber), MESSAGE_NOT_DISPLAYED,
304                             Long.toString(lastReceivedTime) },
305                     null /* sortOrder */)) {
306                 if (cursor != null) {
307                     while (cursor.moveToNext()) {
308                         cbMessages.add(SmsCbMessage.createFromCursor(cursor));
309                         cbMessageUris.add(ContentUris.withAppendedId(CellBroadcasts.CONTENT_URI,
310                                 cursor.getInt(cursor.getColumnIndex(CellBroadcasts._ID))));
311                     }
312                 }
313             }
314         }
315 
316         log("Found " + cbMessages.size() + " not broadcasted messages since "
317                 + DateFormat.getDateTimeInstance().format(lastReceivedTime));
318 
319         List<Geometry> commonBroadcastArea = new ArrayList<>();
320         if (geoFencingTriggerMessage.shouldShareBroadcastArea()) {
321             for (SmsCbMessage msg : cbMessages) {
322                 if (msg.getGeometries() != null) {
323                     commonBroadcastArea.addAll(msg.getGeometries());
324                 }
325             }
326         }
327 
328         // ATIS doesn't specify the geo fencing maximum wait time for the cell broadcasts specified
329         // in geo fencing trigger message. We will pick the largest maximum wait time among these
330         // cell broadcasts.
331         int maxWaitingTimeSec = 0;
332         for (SmsCbMessage msg : cbMessages) {
333             maxWaitingTimeSec = Math.max(maxWaitingTimeSec, getMaxLocationWaitingTime(msg));
334         }
335 
336         if (DBG) {
337             logd("Geo-fencing trigger message = " + geoFencingTriggerMessage);
338             for (SmsCbMessage msg : cbMessages) {
339                 logd(msg.toString());
340             }
341         }
342 
343         if (cbMessages.isEmpty()) {
344             if (DBG) logd("No CellBroadcast message need to be broadcasted");
345             return false;
346         }
347 
348         //Create calculators for each message that will be reused on every location update.
349         CbSendMessageCalculator[] calculators = new CbSendMessageCalculator[cbMessages.size()];
350         for (int i = 0; i < cbMessages.size(); i++) {
351             List<Geometry> broadcastArea = !commonBroadcastArea.isEmpty()
352                     ? commonBroadcastArea : cbMessages.get(i).getGeometries();
353             if (broadcastArea == null) {
354                 broadcastArea = new ArrayList<>();
355             }
356             calculators[i] = mCbSendMessageCalculatorFactory.createNew(mContext, broadcastArea);
357         }
358 
359         requestLocationUpdate(new LocationUpdateCallback() {
360             @Override
361             public void onLocationUpdate(@NonNull CbGeoUtils.LatLng location,
362                     float accuracy) {
363                 if (VDBG) {
364                     logd("onLocationUpdate: location=" + location
365                             + ", acc=" + accuracy + ". ");
366                 }
367                 for (int i = 0; i < cbMessages.size(); i++) {
368                     CbSendMessageCalculator calculator = calculators[i];
369                     if (calculator.getFences().isEmpty()) {
370                         broadcastGeofenceMessage(cbMessages.get(i), cbMessageUris.get(i),
371                                 slotIndex, calculator);
372                     } else {
373                         performGeoFencing(cbMessages.get(i), cbMessageUris.get(i),
374                                 calculator, location, slotIndex, accuracy);
375                     }
376                 }
377 
378                 boolean containsAnyAmbiguousMessages = Arrays.stream(calculators)
379                         .anyMatch(c -> isMessageInAmbiguousState(c));
380                 if (!containsAnyAmbiguousMessages) {
381                     cancelLocationRequest();
382                 }
383             }
384 
385             @Override
386             public void onLocationUnavailable() {
387                 for (int i = 0; i < cbMessages.size(); i++) {
388                     GsmCellBroadcastHandler.this.onLocationUnavailable(calculators[i],
389                             cbMessages.get(i), cbMessageUris.get(i), slotIndex);
390                 }
391             }
392         }, maxWaitingTimeSec);
393         return true;
394     }
395 
396     /**
397      * Process area info message.
398      *
399      * @param slotIndex SIM slot index
400      * @param message Cell broadcast message
401      * @return {@code true} if the mssage is an area info message and got processed correctly,
402      * otherwise {@code false}.
403      */
handleAreaInfoMessage(int slotIndex, SmsCbMessage message)404     private boolean handleAreaInfoMessage(int slotIndex, SmsCbMessage message) {
405         Resources res = getResources(message.getSubscriptionId());
406         int[] areaInfoChannels = res.getIntArray(R.array.area_info_channels);
407 
408         // Check area info message
409         if (IntStream.of(areaInfoChannels).anyMatch(
410                 x -> x == message.getServiceCategory())) {
411             synchronized (mAreaInfos) {
412                 String info = mAreaInfos.get(slotIndex);
413                 if (TextUtils.equals(info, message.getMessageBody())) {
414                     // Message is a duplicate
415                     return true;
416                 }
417                 mAreaInfos.put(slotIndex, message.getMessageBody());
418             }
419 
420             String[] pkgs = mContext.getResources().getStringArray(
421                     R.array.config_area_info_receiver_packages);
422             for (String pkg : pkgs) {
423                 Intent intent = new Intent(CellBroadcastIntents.ACTION_AREA_INFO_UPDATED);
424                 intent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, slotIndex);
425                 intent.setPackage(pkg);
426                 mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
427                         android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
428             }
429             return true;
430         }
431 
432         // This is not an area info message.
433         return false;
434     }
435 
436     /**
437      * Handle 3GPP-format Cell Broadcast messages sent from radio.
438      *
439      * @param message the message to process
440      * @return true if need to wait for geo-fencing or an ordered broadcast was sent.
441      */
442     @Override
handleSmsMessage(Message message)443     protected boolean handleSmsMessage(Message message) {
444         // For GSM, message.obj should be a byte[]
445         int slotIndex = message.arg1;
446         if (message.obj instanceof byte[]) {
447             byte[] pdu = (byte[]) message.obj;
448             SmsCbHeader header = createSmsCbHeader(pdu);
449             if (header == null) return false;
450 
451             log("header=" + header);
452             if (header.getServiceCategory() == SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) {
453                 GeoFencingTriggerMessage triggerMessage =
454                         GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu);
455                 if (triggerMessage != null) {
456                     return handleGeoFencingTriggerMessage(triggerMessage, slotIndex);
457                 }
458             } else {
459                 SmsCbMessage cbMessage = handleGsmBroadcastSms(header, pdu, slotIndex);
460                 if (cbMessage != null) {
461                     if (isDuplicate(cbMessage)) {
462                         CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_FILTERED,
463                                 CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__TYPE__GSM,
464                                 CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__FILTER__DUPLICATE_MESSAGE);
465                         return false;
466                     }
467 
468                     if (handleAreaInfoMessage(slotIndex, cbMessage)) {
469                         log("Channel " + cbMessage.getServiceCategory() + " message processed");
470                         CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_FILTERED,
471                                 CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__TYPE__GSM,
472                                 CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__FILTER__AREA_INFO_MESSAGE);
473                         return false;
474                     }
475 
476                     handleBroadcastSms(cbMessage);
477                     return true;
478                 }
479                 if (VDBG) log("Not handled GSM broadcasts.");
480             }
481         } else {
482             final String errorMessage = "handleSmsMessage for GSM got object of type: "
483                     + message.obj.getClass().getName();
484             loge(errorMessage);
485             CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR,
486                     CELL_BROADCAST_MESSAGE_ERROR__TYPE__UNEXPECTED_GSM_MESSAGE_TYPE_FROM_FWK,
487                     errorMessage);
488         }
489         if (message.obj instanceof SmsCbMessage) {
490             return super.handleSmsMessage(message);
491         } else {
492             return false;
493         }
494     }
495 
496     /**
497      * Get LAC (location area code for GSM/UMTS) / TAC (tracking area code for LTE/NR) and CID
498      * (Cell id) from the cell identity
499      *
500      * @param ci Cell identity
501      * @return Pair of LAC and CID. {@code null} if not available.
502      */
getLacAndCid(CellIdentity ci)503     private @Nullable Pair<Integer, Integer> getLacAndCid(CellIdentity ci) {
504         if (ci == null) return null;
505         int lac = CellInfo.UNAVAILABLE;
506         int cid = CellInfo.UNAVAILABLE;
507         if (ci instanceof CellIdentityGsm) {
508             lac = ((CellIdentityGsm) ci).getLac();
509             cid = ((CellIdentityGsm) ci).getCid();
510         } else if (ci instanceof CellIdentityWcdma) {
511             lac = ((CellIdentityWcdma) ci).getLac();
512             cid = ((CellIdentityWcdma) ci).getCid();
513         } else if ((ci instanceof CellIdentityTdscdma)) {
514             lac = ((CellIdentityTdscdma) ci).getLac();
515             cid = ((CellIdentityTdscdma) ci).getCid();
516         } else if (ci instanceof CellIdentityLte) {
517             lac = ((CellIdentityLte) ci).getTac();
518             cid = ((CellIdentityLte) ci).getCi();
519         } else if (ci instanceof CellIdentityNr) {
520             lac = ((CellIdentityNr) ci).getTac();
521             cid = ((CellIdentityNr) ci).getPci();
522         }
523 
524         if (lac != CellInfo.UNAVAILABLE || cid != CellInfo.UNAVAILABLE) {
525             return Pair.create(lac, cid);
526         }
527 
528         // When both LAC and CID are not available.
529         return null;
530     }
531 
532     /**
533      * Get LAC (location area code for GSM/UMTS) / TAC (tracking area code for LTE/NR) and CID
534      * (Cell id) of the registered network.
535      *
536      * @param slotIndex SIM slot index
537      *
538      * @return lac and cid. {@code null} if cell identity is not available from the registered
539      * network.
540      */
getLacAndCid(int slotIndex)541     private @Nullable Pair<Integer, Integer> getLacAndCid(int slotIndex) {
542         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
543         tm.createForSubscriptionId(getSubIdForPhone(mContext, slotIndex));
544 
545         ServiceState serviceState = tm.getServiceState();
546 
547         if (serviceState == null) return null;
548 
549         // The list of cell identity to extract LAC and CID. The higher priority one will be added
550         // into the top of list.
551         List<CellIdentity> cellIdentityList = new ArrayList<>();
552 
553         // CS network
554         NetworkRegistrationInfo nri = serviceState.getNetworkRegistrationInfo(
555                 NetworkRegistrationInfo.DOMAIN_CS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
556         if (nri != null) {
557             cellIdentityList.add(nri.getCellIdentity());
558         }
559 
560         // PS network
561         nri = serviceState.getNetworkRegistrationInfo(
562                 NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
563         if (nri != null) {
564             cellIdentityList.add(nri.getCellIdentity());
565         }
566 
567         // When SIM is not inserted, we use the cell identity from the nearby cell. This is
568         // best effort.
569         List<CellInfo> infos = tm.getAllCellInfo();
570         if (infos != null) {
571             cellIdentityList.addAll(
572                     infos.stream().map(CellInfo::getCellIdentity).collect(Collectors.toList()));
573         }
574 
575         // Return the first valid LAC and CID from the list.
576         return cellIdentityList.stream()
577                 .map(this::getLacAndCid)
578                 .filter(Objects::nonNull)
579                 .findFirst()
580                 .orElse(null);
581     }
582 
583 
584     /**
585      * Handle 3GPP format SMS-CB message.
586      * @param header the cellbroadcast header.
587      * @param receivedPdu the received PDUs as a byte[]
588      */
handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu, int slotIndex)589     private SmsCbMessage handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu,
590             int slotIndex) {
591         try {
592             if (VDBG) {
593                 int pduLength = receivedPdu.length;
594                 for (int i = 0; i < pduLength; i += 8) {
595                     StringBuilder sb = new StringBuilder("SMS CB pdu data: ");
596                     for (int j = i; j < i + 8 && j < pduLength; j++) {
597                         int b = receivedPdu[j] & 0xff;
598                         if (b < 0x10) {
599                             sb.append('0');
600                         }
601                         sb.append(Integer.toHexString(b)).append(' ');
602                     }
603                     log(sb.toString());
604                 }
605             }
606 
607             if (VDBG) log("header=" + header);
608             TelephonyManager tm =
609                     (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
610             tm.createForSubscriptionId(getSubIdForPhone(mContext, slotIndex));
611             String plmn = tm.getNetworkOperator();
612             int lac = -1;
613             int cid = -1;
614             // Get LAC and CID of the current camped cell.
615             Pair<Integer, Integer> lacAndCid = getLacAndCid(slotIndex);
616             if (lacAndCid != null) {
617                 lac = lacAndCid.first;
618                 cid = lacAndCid.second;
619             }
620 
621             SmsCbLocation location = new SmsCbLocation(plmn, lac, cid);
622 
623             byte[][] pdus;
624             int pageCount = header.getNumberOfPages();
625             if (pageCount > 1) {
626                 // Multi-page message
627                 SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location);
628 
629                 // Try to find other pages of the same message
630                 pdus = mSmsCbPageMap.get(concatInfo);
631 
632                 if (pdus == null) {
633                     // This is the first page of this message, make room for all
634                     // pages and keep until complete
635                     pdus = new byte[pageCount][];
636 
637                     mSmsCbPageMap.put(concatInfo, pdus);
638                 }
639 
640                 if (VDBG) log("pdus size=" + pdus.length);
641                 // Page parameter is one-based
642                 pdus[header.getPageIndex() - 1] = receivedPdu;
643 
644                 for (byte[] pdu : pdus) {
645                     if (pdu == null) {
646                         // Still missing pages, exit
647                         log("still missing pdu");
648                         return null;
649                     }
650                 }
651 
652                 // Message complete, remove and dispatch
653                 mSmsCbPageMap.remove(concatInfo);
654             } else {
655                 // Single page message
656                 pdus = new byte[1][];
657                 pdus[0] = receivedPdu;
658             }
659 
660             // Remove messages that are out of scope to prevent the map from
661             // growing indefinitely, containing incomplete messages that were
662             // never assembled
663             Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator();
664 
665             while (iter.hasNext()) {
666                 SmsCbConcatInfo info = iter.next();
667 
668                 if (!info.matchesLocation(plmn, lac, cid)) {
669                     iter.remove();
670                 }
671             }
672 
673             return GsmSmsCbMessage.createSmsCbMessage(mContext, header, location, pdus, slotIndex);
674 
675         } catch (RuntimeException e) {
676             final String errorMessage = "Error in decoding SMS CB pdu: " + e.toString();
677             e.printStackTrace();
678             loge(errorMessage);
679             CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR,
680                     CELL_BROADCAST_MESSAGE_ERROR__TYPE__GSM_INVALID_PDU, errorMessage);
681             return null;
682         }
683     }
684 
createSmsCbHeader(byte[] bytes)685     private SmsCbHeader createSmsCbHeader(byte[] bytes) {
686         try {
687             return new SmsCbHeader(bytes);
688         } catch (Exception ex) {
689             loge("Can't create SmsCbHeader, ex = " + ex.toString());
690             return null;
691         }
692     }
693 
694     private BroadcastReceiver mGsmReceiver = new BroadcastReceiver() {
695         @Override
696         public void onReceive(Context context, Intent intent) {
697             switch (intent.getAction()) {
698                 case ACTION_AREA_UPDATE_ENABLED:
699                     boolean enabled = intent.getBooleanExtra(EXTRA_ENABLE, false);
700                     log("Area update info enabled: " + enabled);
701                     String[] pkgs = mContext.getResources().getStringArray(
702                             R.array.config_area_info_receiver_packages);
703                     // set mAreaInfo to null before sending the broadcast to listeners to avoid
704                     // possible race condition.
705                     if (!enabled) {
706                         mAreaInfos.clear();
707                         log("Area update info disabled, clear areaInfo");
708                     }
709                     // notify receivers. the setting is singleton for msim devices, if areaInfo
710                     // toggle was off/on, it will applies for all slots/subscriptions.
711                     TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
712                     for(int i = 0; i < tm.getActiveModemCount(); i++) {
713                         for (String pkg : pkgs) {
714                             Intent areaInfoIntent = new Intent(
715                                     CellBroadcastIntents.ACTION_AREA_INFO_UPDATED);
716                             areaInfoIntent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, i);
717                             areaInfoIntent.putExtra(EXTRA_ENABLE, enabled);
718                             areaInfoIntent.setPackage(pkg);
719                             mContext.sendBroadcastAsUser(areaInfoIntent, UserHandle.ALL,
720                                     android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
721                         }
722                     }
723                     break;
724                 case CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED:
725                     // (Okay to use res for slot 0 here because this should not be carrier specific)
726                     if (getResourcesForSlot(0).getBoolean(R.bool.reset_area_info_on_oos)) {
727                         registerServiceStateListeners();
728                     }
729                     break;
730                 default:
731                     log("Unhandled broadcast " + intent.getAction());
732             }
733         }
734     };
735 
736     /**
737      * Holds all info about a message page needed to assemble a complete concatenated message.
738      */
739     @VisibleForTesting
740     public static final class SmsCbConcatInfo {
741 
742         private final SmsCbHeader mHeader;
743         private final SmsCbLocation mLocation;
744 
745         @VisibleForTesting
SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location)746         public SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) {
747             mHeader = header;
748             mLocation = location;
749         }
750 
751         @Override
hashCode()752         public int hashCode() {
753             return (mHeader.getSerialNumber() * 31) + mLocation.hashCode();
754         }
755 
756         @Override
equals(Object obj)757         public boolean equals(Object obj) {
758             if (obj instanceof SmsCbConcatInfo) {
759                 SmsCbConcatInfo other = (SmsCbConcatInfo) obj;
760 
761                 // Two pages match if they have the same serial number (which includes the
762                 // geographical scope and update number), and both pages belong to the same
763                 // location (PLMN, plus LAC and CID if these are part of the geographical scope).
764                 return mHeader.getSerialNumber() == other.mHeader.getSerialNumber()
765                         && mLocation.equals(other.mLocation);
766             }
767 
768             return false;
769         }
770 
771         /**
772          * Compare the location code for this message to the current location code. The match is
773          * relative to the geographical scope of the message, which determines whether the LAC
774          * and Cell ID are saved in mLocation or set to -1 to match all values.
775          *
776          * @param plmn the current PLMN
777          * @param lac the current Location Area (GSM) or Service Area (UMTS)
778          * @param cid the current Cell ID
779          * @return true if this message is valid for the current location; false otherwise
780          */
matchesLocation(String plmn, int lac, int cid)781         public boolean matchesLocation(String plmn, int lac, int cid) {
782             return mLocation.isInLocationArea(plmn, lac, cid);
783         }
784     }
785 }
786