| /* |
| * 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<String> mAreaInfos = new SparseArray<>(); |
| |
| /** |
| * Used to store ServiceStateListeners for each active slot |
| */ |
| private final SparseArray<ServiceStateListener> mServiceStateListener = new SparseArray<>(); |
| |
| /** This map holds incomplete concatenated messages waiting for assembly. */ |
| private final HashMap<SmsCbConcatInfo, byte[][]> 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, Context.RECEIVER_NOT_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, Context.RECEIVER_NOT_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() { |
| // 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); |
| } |
| } |
| |
| 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[] subIds = subMgr.getSubscriptionIds(slotIndex); |
| Resources res; |
| if (subIds != null) { |
| res = getResources(subIds[0]); |
| } 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<SmsCbMessage> cbMessages = new ArrayList<>(); |
| final List<Uri> 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<Geometry> 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<Geometry> 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<Integer, Integer> 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<Integer, Integer> 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<CellIdentity> 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<CellInfo> 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<Integer, Integer> 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<SmsCbConcatInfo> 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); |
| } |
| } |