/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.cellbroadcastservice; import static com.android.cellbroadcastservice.CellBroadcastMetrics.ERR_GSM_INVALID_PDU; import static com.android.cellbroadcastservice.CellBroadcastMetrics.ERR_UNEXPECTED_GSM_MSG_FROM_FWK; import static com.android.cellbroadcastservice.CellBroadcastMetrics.FILTER_AREAINFO; import static com.android.cellbroadcastservice.CellBroadcastMetrics.FILTER_DUPLICATE; import static com.android.cellbroadcastservice.CellBroadcastMetrics.RPT_GSM; import static com.android.cellbroadcastservice.CellBroadcastMetrics.SRC_CBS; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.os.Looper; import android.os.Message; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Telephony.CellBroadcasts; import android.telephony.AccessNetworkConstants; import android.telephony.CbGeoUtils; import android.telephony.CbGeoUtils.Geometry; import android.telephony.CellBroadcastIntents; import android.telephony.CellIdentity; import android.telephony.CellIdentityGsm; import android.telephony.CellIdentityLte; import android.telephony.CellIdentityNr; import android.telephony.CellIdentityTdscdma; import android.telephony.CellIdentityWcdma; import android.telephony.CellInfo; import android.telephony.NetworkRegistrationInfo; import android.telephony.PhoneStateListener; import android.telephony.ServiceState; import android.telephony.SmsCbLocation; import android.telephony.SmsCbMessage; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Pair; import android.util.SparseArray; import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage; import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity; import com.android.internal.annotations.VisibleForTesting; import java.io.FileDescriptor; import java.io.PrintWriter; import java.text.DateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.IntStream; /** * Handler for 3GPP format Cell Broadcasts. Parent class can also handle CDMA Cell Broadcasts. */ public class GsmCellBroadcastHandler extends CellBroadcastHandler { private static final boolean VDBG = false; // log CB PDU data /** Indicates that a message is not displayed. */ private static final String MESSAGE_NOT_DISPLAYED = "0"; /** * Intent sent from cellbroadcastreceiver to notify cellbroadcastservice that area info update * is disabled/enabled. */ private static final String ACTION_AREA_UPDATE_ENABLED = "com.android.cellbroadcastreceiver.action.AREA_UPDATE_INFO_ENABLED"; /** * The extra for cell ACTION_AREA_UPDATE_ENABLED enable/disable */ private static final String EXTRA_ENABLE = "enable"; /** * This permission is only granted to the cellbroadcast mainline module and thus can be * used for permission check within CBR and CBS. */ private static final String CBR_MODULE_PERMISSION = "com.android.cellbroadcastservice.FULL_ACCESS_CELL_BROADCAST_HISTORY"; private final SparseArray mAreaInfos = new SparseArray<>(); /** * Used to store ServiceStateListeners for each active slot */ private final SparseArray mServiceStateListener = new SparseArray<>(); /** This map holds incomplete concatenated messages waiting for assembly. */ private final HashMap mSmsCbPageMap = new HashMap<>(4); private boolean mIsResetAreaInfoOnOos; @VisibleForTesting public GsmCellBroadcastHandler(Context context, Looper looper, CbSendMessageCalculatorFactory cbSendMessageCalculatorFactory, CellBroadcastHandler.HandlerHelper handlerHelper) { super("GsmCellBroadcastHandler", context, looper, cbSendMessageCalculatorFactory, handlerHelper); mContext.registerReceiver(mGsmReceiver, new IntentFilter(ACTION_AREA_UPDATE_ENABLED), CBR_MODULE_PERMISSION, null, RECEIVER_EXPORTED); mContext.registerReceiver(mGsmReceiver, new IntentFilter(SubscriptionManager.ACTION_DEFAULT_SUBSCRIPTION_CHANGED), null, null); loadConfig(SubscriptionManager.getDefaultSubscriptionId()); } /** * Constructor used only for tests. This constructor allows the caller to pass in resources * and a subId to be put into the resources cache before getResourcesForSlot called (this is * needed for unit tests to prevent */ @VisibleForTesting public GsmCellBroadcastHandler(Context context, Looper looper, CbSendMessageCalculatorFactory cbSendMessageCalculatorFactory, CellBroadcastHandler.HandlerHelper handlerHelper, Resources resources, int subId) { super("GsmCellBroadcastHandler", context, looper, cbSendMessageCalculatorFactory, handlerHelper); mContext.registerReceiver(mGsmReceiver, new IntentFilter(ACTION_AREA_UPDATE_ENABLED), CBR_MODULE_PERMISSION, null, RECEIVER_EXPORTED); mContext.registerReceiver(mGsmReceiver, new IntentFilter(SubscriptionManager.ACTION_DEFAULT_SUBSCRIPTION_CHANGED), null, null); // set the resources cache here for unit tests mResourcesCache.put(subId, resources); loadConfig(subId); } @Override public void cleanup() { log("cleanup"); unregisterServiceStateListeners(); mContext.unregisterReceiver(mGsmReceiver); super.cleanup(); } private void loadConfig(int subId) { // Some OEMs want us to reset the area info updates when going out of service. // The config is loaded from the resource of the default sub id. if (!SubscriptionManager.isValidSubscriptionId(subId)) { log("subId[" + subId + "] is not valid"); return; } mIsResetAreaInfoOnOos = getResources(subId).getBoolean(R.bool.reset_area_info_on_oos); if (mIsResetAreaInfoOnOos) { registerServiceStateListeners(); } else { unregisterServiceStateListeners(); } CellBroadcastServiceMetrics.getInstance().getFeatureMetrics(mContext) .onChangedResetAreaInfo(mIsResetAreaInfoOnOos); } private void registerServiceStateListeners() { // clean previously registered listeners unregisterServiceStateListeners(); // register for all active slots TelephonyManager tm = mContext.getSystemService(TelephonyManager.class); SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class); for (int slotId = 0; slotId < tm.getActiveModemCount(); slotId++) { SubscriptionInfo info = sm.getActiveSubscriptionInfoForSimSlotIndex(slotId); if (info != null) { int subId = info.getSubscriptionId(); if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { mServiceStateListener.put(slotId, new ServiceStateListener(subId, slotId)); tm.createForSubscriptionId(subId).listen(mServiceStateListener.get(slotId), PhoneStateListener.LISTEN_SERVICE_STATE); } } } } private void unregisterServiceStateListeners() { TelephonyManager tm = mContext.getSystemService(TelephonyManager.class); int size = mServiceStateListener.size(); for (int i = 0; i < size; i++) { tm.listen(mServiceStateListener.valueAt(i), PhoneStateListener.LISTEN_NONE); } mServiceStateListener.clear(); } private class ServiceStateListener extends PhoneStateListener { // subId is not needed for clearing area info, only used for debugging purposes private int mSubId; private int mSlotId; ServiceStateListener(int subId, int slotId) { mSubId = subId; mSlotId = slotId; } @Override public void onServiceStateChanged(@NonNull ServiceState serviceState) { int state = serviceState.getState(); if (state == ServiceState.STATE_POWER_OFF || state == ServiceState.STATE_OUT_OF_SERVICE || state == ServiceState.STATE_EMERGENCY_ONLY) { synchronized (mAreaInfos) { if (mAreaInfos.contains(mSlotId)) { log("OOS state=" + state + " mSubId=" + mSubId + " mSlotId=" + mSlotId + ", clearing area infos"); mAreaInfos.remove(mSlotId); } } } } } @Override protected void onQuitting() { super.onQuitting(); // release wakelock } /** * Handle a GSM cell broadcast message passed from the telephony framework. * @param message */ public void onGsmCellBroadcastSms(int slotIndex, byte[] message) { sendMessage(EVENT_NEW_SMS_MESSAGE, slotIndex, -1, message); } /** * Get the area information * * @param slotIndex SIM slot index * @return The area information */ @NonNull public String getCellBroadcastAreaInfo(int slotIndex) { String info; synchronized (mAreaInfos) { info = mAreaInfos.get(slotIndex); } return info == null ? "" : info; } /** * Set the area information * * @param slotIndex SIM slot index * @param info area info for the slot */ @VisibleForTesting public void setCellBroadcastAreaInfo(int slotIndex, String info) { synchronized (mAreaInfos) { mAreaInfos.put(slotIndex, info); } } /** * Create a new CellBroadcastHandler. * @param context the context to use for dispatching Intents * @return the new handler */ public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context) { GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, Looper.myLooper(), new CbSendMessageCalculatorFactory(), null); handler.start(); return handler; } private Resources getResourcesForSlot(int slotIndex) { SubscriptionManager subMgr = mContext.getSystemService(SubscriptionManager.class); int subId = getSubIdForPhone(mContext, slotIndex); Resources res; if (SubscriptionManager.isValidSubscriptionId(subId)) { res = getResources(subId); } else { res = getResources(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); } return res; } /** * Find the cell broadcast messages specify by the geo-fencing trigger message and perform a * geo-fencing check for these messages. * @param geoFencingTriggerMessage the trigger message * * @return {@code True} if geo-fencing is need for some cell broadcast message. */ private boolean handleGeoFencingTriggerMessage( GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex) { final List cbMessages = new ArrayList<>(); final List cbMessageUris = new ArrayList<>(); Resources res = getResourcesForSlot(slotIndex); // Only consider the cell broadcast received within 24 hours. long lastReceivedTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS; // Some carriers require reset duplication detection after airplane mode or reboot. if (res.getBoolean(R.bool.reset_on_power_cycle_or_airplane_mode)) { lastReceivedTime = Long.max(lastReceivedTime, mLastAirplaneModeTime); lastReceivedTime = Long.max(lastReceivedTime, System.currentTimeMillis() - SystemClock.elapsedRealtime()); } // Find the cell broadcast message identify by the message identifier and serial number // and was not displayed. String where = CellBroadcasts.SERVICE_CATEGORY + "=? AND " + CellBroadcasts.SERIAL_NUMBER + "=? AND " + CellBroadcasts.MESSAGE_DISPLAYED + "=? AND " + CellBroadcasts.RECEIVED_TIME + ">?"; ContentResolver resolver = mContext.getContentResolver(); for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) { try (Cursor cursor = resolver.query(CellBroadcasts.CONTENT_URI, CellBroadcastProvider.QUERY_COLUMNS, where, new String[] { Integer.toString(identity.messageIdentifier), Integer.toString(identity.serialNumber), MESSAGE_NOT_DISPLAYED, Long.toString(lastReceivedTime) }, null /* sortOrder */)) { if (cursor != null) { while (cursor.moveToNext()) { cbMessages.add(SmsCbMessage.createFromCursor(cursor)); cbMessageUris.add(ContentUris.withAppendedId(CellBroadcasts.CONTENT_URI, cursor.getInt(cursor.getColumnIndex(CellBroadcasts._ID)))); } } } } log("Found " + cbMessages.size() + " not broadcasted messages since " + DateFormat.getDateTimeInstance().format(lastReceivedTime)); List commonBroadcastArea = new ArrayList<>(); if (geoFencingTriggerMessage.shouldShareBroadcastArea()) { for (SmsCbMessage msg : cbMessages) { if (msg.getGeometries() != null) { commonBroadcastArea.addAll(msg.getGeometries()); } } } // ATIS doesn't specify the geo fencing maximum wait time for the cell broadcasts specified // in geo fencing trigger message. We will pick the largest maximum wait time among these // cell broadcasts. int maxWaitingTimeSec = 0; for (SmsCbMessage msg : cbMessages) { maxWaitingTimeSec = Math.max(maxWaitingTimeSec, getMaxLocationWaitingTime(msg)); } if (DBG) { logd("Geo-fencing trigger message = " + geoFencingTriggerMessage); for (SmsCbMessage msg : cbMessages) { logd(msg.toString()); } } if (cbMessages.isEmpty()) { if (DBG) logd("No CellBroadcast message need to be broadcasted"); return false; } //Create calculators for each message that will be reused on every location update. CbSendMessageCalculator[] calculators = new CbSendMessageCalculator[cbMessages.size()]; for (int i = 0; i < cbMessages.size(); i++) { List broadcastArea = !commonBroadcastArea.isEmpty() ? commonBroadcastArea : cbMessages.get(i).getGeometries(); if (broadcastArea == null) { broadcastArea = new ArrayList<>(); } calculators[i] = mCbSendMessageCalculatorFactory.createNew(mContext, broadcastArea); } requestLocationUpdate(new LocationUpdateCallback() { @Override public void onLocationUpdate(@NonNull CbGeoUtils.LatLng location, float accuracy) { if (VDBG) { logd("onLocationUpdate: location=" + location + ", acc=" + accuracy + ". "); } for (int i = 0; i < cbMessages.size(); i++) { CbSendMessageCalculator calculator = calculators[i]; if (calculator.getFences().isEmpty()) { broadcastGeofenceMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex, calculator); } else { performGeoFencing(cbMessages.get(i), cbMessageUris.get(i), calculator, location, slotIndex, accuracy); } } } @Override public boolean areAllMessagesHandled() { boolean containsAnyAmbiguousMessages = Arrays.stream(calculators) .anyMatch(c -> isMessageInAmbiguousState(c)); return !containsAnyAmbiguousMessages; } @Override public void onLocationUnavailable() { for (int i = 0; i < cbMessages.size(); i++) { GsmCellBroadcastHandler.this.onLocationUnavailable(calculators[i], cbMessages.get(i), cbMessageUris.get(i), slotIndex); } } }, maxWaitingTimeSec); return true; } /** * Process area info message. * * @param slotIndex SIM slot index * @param message Cell broadcast message * @return {@code true} if the mssage is an area info message and got processed correctly, * otherwise {@code false}. */ private boolean handleAreaInfoMessage(int slotIndex, SmsCbMessage message) { Resources res = getResources(message.getSubscriptionId()); int[] areaInfoChannels = res.getIntArray(R.array.area_info_channels); // Check area info message if (IntStream.of(areaInfoChannels).anyMatch( x -> x == message.getServiceCategory())) { synchronized (mAreaInfos) { String info = mAreaInfos.get(slotIndex); if (TextUtils.equals(info, message.getMessageBody())) { // Message is a duplicate return true; } mAreaInfos.put(slotIndex, message.getMessageBody()); } String[] pkgs = mContext.getResources().getStringArray( R.array.config_area_info_receiver_packages); CellBroadcastServiceMetrics.getInstance().getFeatureMetrics(mContext) .onChangedAreaInfoPackage(new ArrayList<>(Arrays.asList(pkgs))); for (String pkg : pkgs) { Intent intent = new Intent(CellBroadcastIntents.ACTION_AREA_INFO_UPDATED); intent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, slotIndex); intent.setPackage(pkg); mContext.sendBroadcastAsUser(intent, UserHandle.ALL, android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE); } return true; } // This is not an area info message. return false; } /** * Handle 3GPP-format Cell Broadcast messages sent from radio. * * @param message the message to process * @return true if need to wait for geo-fencing or an ordered broadcast was sent. */ @Override protected boolean handleSmsMessage(Message message) { // For GSM, message.obj should be a byte[] int slotIndex = message.arg1; if (message.obj instanceof byte[]) { byte[] pdu = (byte[]) message.obj; SmsCbHeader header = createSmsCbHeader(pdu); if (header == null) return false; CellBroadcastServiceMetrics.getInstance().logMessageReported(mContext, RPT_GSM, SRC_CBS, header.getSerialNumber(), header.getServiceCategory()); if (header.getServiceCategory() == SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) { GeoFencingTriggerMessage triggerMessage = GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu); if (triggerMessage != null) { return handleGeoFencingTriggerMessage(triggerMessage, slotIndex); } } else { SmsCbMessage cbMessage = handleGsmBroadcastSms(header, pdu, slotIndex); if (cbMessage != null) { if (isDuplicate(cbMessage)) { CellBroadcastServiceMetrics.getInstance() .logMessageFiltered(FILTER_DUPLICATE, cbMessage); return false; } if (handleAreaInfoMessage(slotIndex, cbMessage)) { log("Channel " + cbMessage.getServiceCategory() + " message processed"); CellBroadcastServiceMetrics.getInstance() .logMessageFiltered(FILTER_AREAINFO, cbMessage); return false; } handleBroadcastSms(cbMessage); return true; } if (VDBG) log("Not handled GSM broadcasts."); } } else { final String errorMessage = "handleSmsMessage for GSM got object of type: " + message.obj.getClass().getName(); loge(errorMessage); CellBroadcastServiceMetrics.getInstance().logMessageError( ERR_UNEXPECTED_GSM_MSG_FROM_FWK, errorMessage); } if (message.obj instanceof SmsCbMessage) { return super.handleSmsMessage(message); } else { return false; } } /** * Get LAC (location area code for GSM/UMTS) / TAC (tracking area code for LTE/NR) and CID * (Cell id) from the cell identity * * @param ci Cell identity * @return Pair of LAC and CID. {@code null} if not available. */ private @Nullable Pair getLacAndCid(CellIdentity ci) { if (ci == null) return null; int lac = CellInfo.UNAVAILABLE; int cid = CellInfo.UNAVAILABLE; if (ci instanceof CellIdentityGsm) { lac = ((CellIdentityGsm) ci).getLac(); cid = ((CellIdentityGsm) ci).getCid(); } else if (ci instanceof CellIdentityWcdma) { lac = ((CellIdentityWcdma) ci).getLac(); cid = ((CellIdentityWcdma) ci).getCid(); } else if ((ci instanceof CellIdentityTdscdma)) { lac = ((CellIdentityTdscdma) ci).getLac(); cid = ((CellIdentityTdscdma) ci).getCid(); } else if (ci instanceof CellIdentityLte) { lac = ((CellIdentityLte) ci).getTac(); cid = ((CellIdentityLte) ci).getCi(); } else if (ci instanceof CellIdentityNr) { lac = ((CellIdentityNr) ci).getTac(); cid = ((CellIdentityNr) ci).getPci(); } if (lac != CellInfo.UNAVAILABLE || cid != CellInfo.UNAVAILABLE) { return Pair.create(lac, cid); } // When both LAC and CID are not available. return null; } /** * Get LAC (location area code for GSM/UMTS) / TAC (tracking area code for LTE/NR) and CID * (Cell id) of the registered network. * * @param slotIndex SIM slot index * * @return lac and cid. {@code null} if cell identity is not available from the registered * network. */ private @Nullable Pair getLacAndCid(int slotIndex) { TelephonyManager tm = mContext.getSystemService(TelephonyManager.class); tm.createForSubscriptionId(getSubIdForPhone(mContext, slotIndex)); ServiceState serviceState = tm.getServiceState(); if (serviceState == null) return null; // The list of cell identity to extract LAC and CID. The higher priority one will be added // into the top of list. List cellIdentityList = new ArrayList<>(); // CS network NetworkRegistrationInfo nri = serviceState.getNetworkRegistrationInfo( NetworkRegistrationInfo.DOMAIN_CS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN); if (nri != null) { cellIdentityList.add(nri.getCellIdentity()); } // PS network nri = serviceState.getNetworkRegistrationInfo( NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN); if (nri != null) { cellIdentityList.add(nri.getCellIdentity()); } // When SIM is not inserted, we use the cell identity from the nearby cell. This is // best effort. List infos = tm.getAllCellInfo(); if (infos != null) { cellIdentityList.addAll( infos.stream().map(CellInfo::getCellIdentity).collect(Collectors.toList())); } // Return the first valid LAC and CID from the list. return cellIdentityList.stream() .map(this::getLacAndCid) .filter(Objects::nonNull) .findFirst() .orElse(null); } /** * Handle 3GPP format SMS-CB message. * @param header the cellbroadcast header. * @param receivedPdu the received PDUs as a byte[] */ private SmsCbMessage handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu, int slotIndex) { try { if (VDBG) { int pduLength = receivedPdu.length; for (int i = 0; i < pduLength; i += 8) { StringBuilder sb = new StringBuilder("SMS CB pdu data: "); for (int j = i; j < i + 8 && j < pduLength; j++) { int b = receivedPdu[j] & 0xff; if (b < 0x10) { sb.append('0'); } sb.append(Integer.toHexString(b)).append(' '); } log(sb.toString()); } } if (VDBG) log("header=" + header); TelephonyManager tm = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); tm.createForSubscriptionId(getSubIdForPhone(mContext, slotIndex)); String plmn = tm.getNetworkOperator(); int lac = -1; int cid = -1; // Get LAC and CID of the current camped cell. Pair lacAndCid = getLacAndCid(slotIndex); if (lacAndCid != null) { lac = lacAndCid.first; cid = lacAndCid.second; } SmsCbLocation location = new SmsCbLocation(plmn, lac, cid); byte[][] pdus; int pageCount = header.getNumberOfPages(); if (pageCount > 1) { // Multi-page message SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location); // Try to find other pages of the same message pdus = mSmsCbPageMap.get(concatInfo); if (pdus == null) { // This is the first page of this message, make room for all // pages and keep until complete pdus = new byte[pageCount][]; mSmsCbPageMap.put(concatInfo, pdus); } if (VDBG) log("pdus size=" + pdus.length); // Page parameter is one-based pdus[header.getPageIndex() - 1] = receivedPdu; for (byte[] pdu : pdus) { if (pdu == null) { // Still missing pages, exit log("still missing pdu"); return null; } } // Message complete, remove and dispatch mSmsCbPageMap.remove(concatInfo); } else { // Single page message pdus = new byte[1][]; pdus[0] = receivedPdu; } // Remove messages that are out of scope to prevent the map from // growing indefinitely, containing incomplete messages that were // never assembled Iterator iter = mSmsCbPageMap.keySet().iterator(); while (iter.hasNext()) { SmsCbConcatInfo info = iter.next(); if (!info.matchesLocation(plmn, lac, cid)) { iter.remove(); } } return GsmSmsCbMessage.createSmsCbMessage(mContext, header, location, pdus, slotIndex); } catch (RuntimeException e) { final String errorMsg = "Error in decoding SMS CB pdu: " + e.toString(); e.printStackTrace(); loge(errorMsg); CellBroadcastServiceMetrics.getInstance() .logMessageError(ERR_GSM_INVALID_PDU, errorMsg); return null; } } private SmsCbHeader createSmsCbHeader(byte[] bytes) { try { return new SmsCbHeader(bytes); } catch (Exception ex) { loge("Can't create SmsCbHeader, ex = " + ex.toString()); return null; } } private BroadcastReceiver mGsmReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { switch (intent.getAction()) { case ACTION_AREA_UPDATE_ENABLED: boolean enabled = intent.getBooleanExtra(EXTRA_ENABLE, false); log("Area update info enabled: " + enabled); String[] pkgs = mContext.getResources().getStringArray( R.array.config_area_info_receiver_packages); // set mAreaInfo to null before sending the broadcast to listeners to avoid // possible race condition. if (!enabled) { mAreaInfos.clear(); log("Area update info disabled, clear areaInfo"); } // notify receivers. the setting is singleton for msim devices, if areaInfo // toggle was off/on, it will applies for all slots/subscriptions. TelephonyManager tm = mContext.getSystemService(TelephonyManager.class); for(int i = 0; i < tm.getActiveModemCount(); i++) { for (String pkg : pkgs) { Intent areaInfoIntent = new Intent( CellBroadcastIntents.ACTION_AREA_INFO_UPDATED); areaInfoIntent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, i); areaInfoIntent.putExtra(EXTRA_ENABLE, enabled); areaInfoIntent.setPackage(pkg); mContext.sendBroadcastAsUser(areaInfoIntent, UserHandle.ALL, android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE); } } break; case SubscriptionManager.ACTION_DEFAULT_SUBSCRIPTION_CHANGED: if (intent.hasExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX)) { loadConfig(intent.getIntExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)); } break; default: log("Unhandled broadcast " + intent.getAction()); } } }; /** * Holds all info about a message page needed to assemble a complete concatenated message. */ @VisibleForTesting public static final class SmsCbConcatInfo { private final SmsCbHeader mHeader; private final SmsCbLocation mLocation; @VisibleForTesting public SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) { mHeader = header; mLocation = location; } @Override public int hashCode() { return (mHeader.getSerialNumber() * 31) + mLocation.hashCode(); } @Override public boolean equals(Object obj) { if (obj instanceof SmsCbConcatInfo) { SmsCbConcatInfo other = (SmsCbConcatInfo) obj; // Two pages match if they have the same serial number (which includes the // geographical scope and update number), and both pages belong to the same // location (PLMN, plus LAC and CID if these are part of the geographical scope). return mHeader.getSerialNumber() == other.mHeader.getSerialNumber() && mLocation.equals(other.mLocation); } return false; } /** * Compare the location code for this message to the current location code. The match is * relative to the geographical scope of the message, which determines whether the LAC * and Cell ID are saved in mLocation or set to -1 to match all values. * * @param plmn the current PLMN * @param lac the current Location Area (GSM) or Service Area (UMTS) * @param cid the current Cell ID * @return true if this message is valid for the current location; false otherwise */ public boolean matchesLocation(String plmn, int lac, int cid) { return mLocation.isInLocationArea(plmn, lac, cid); } } @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("GsmCellBroadcastHandler:"); pw.println(" mAreaInfos=:" + mAreaInfos); pw.println(" mSmsCbPageMap=:" + mSmsCbPageMap); super.dump(fd, pw, args); } }