/* * 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.internal.telephony.gsm; import android.content.Context; import android.os.AsyncResult; import android.os.Message; import android.os.SystemProperties; import android.telephony.CellLocation; import android.telephony.SmsCbLocation; import android.telephony.SmsCbMessage; import android.telephony.gsm.GsmCellLocation; import android.telephony.TelephonyManager; import com.android.internal.telephony.CellBroadcastHandler; import com.android.internal.telephony.PhoneBase; import com.android.internal.telephony.TelephonyProperties; import java.util.HashMap; import java.util.Iterator; /** * 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 /** This map holds incomplete concatenated messages waiting for assembly. */ private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap = new HashMap<SmsCbConcatInfo, byte[][]>(4); protected GsmCellBroadcastHandler(Context context, PhoneBase phone) { super("GsmCellBroadcastHandler", context, phone); phone.mCi.setOnNewGsmBroadcastSms(getHandler(), EVENT_NEW_SMS_MESSAGE, null); } @Override protected void onQuitting() { mPhone.mCi.unSetOnNewGsmBroadcastSms(getHandler()); super.onQuitting(); // release wakelock } /** * Create a new CellBroadcastHandler. * @param context the context to use for dispatching Intents * @return the new handler */ public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context, PhoneBase phone) { GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, phone); handler.start(); return handler; } /** * Handle 3GPP-format Cell Broadcast messages sent from radio. * * @param message the message to process * @return true if an ordered broadcast was sent; false on failure */ @Override protected boolean handleSmsMessage(Message message) { if (message.obj instanceof AsyncResult) { SmsCbMessage cbMessage = handleGsmBroadcastSms((AsyncResult) message.obj); if (cbMessage != null) { handleBroadcastSms(cbMessage); return true; } } return super.handleSmsMessage(message); } /** * Handle 3GPP format SMS-CB message. * @param ar the AsyncResult containing the received PDUs */ private SmsCbMessage handleGsmBroadcastSms(AsyncResult ar) { try { byte[] receivedPdu = (byte[]) ar.result; 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()); } } SmsCbHeader header = new SmsCbHeader(receivedPdu); String plmn = TelephonyManager.from(mContext).getNetworkOperatorForPhone( mPhone.getPhoneId()); int lac = -1; int cid = -1; CellLocation cl = mPhone.getCellLocation(); // Check if cell location is GsmCellLocation. This is required to support // dual-mode devices such as CDMA/LTE devices that require support for // both 3GPP and 3GPP2 format messages if (cl instanceof GsmCellLocation) { GsmCellLocation cellLocation = (GsmCellLocation)cl; lac = cellLocation.getLac(); cid = cellLocation.getCid(); } SmsCbLocation location; switch (header.getGeographicalScope()) { case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE: location = new SmsCbLocation(plmn, lac, -1); break; case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE: case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE: location = new SmsCbLocation(plmn, lac, cid); break; case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE: default: location = new SmsCbLocation(plmn); break; } 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); } // Page parameter is one-based pdus[header.getPageIndex() - 1] = receivedPdu; for (byte[] pdu : pdus) { if (pdu == null) { // Still missing pages, exit 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(header, location, pdus); } catch (RuntimeException e) { loge("Error in decoding SMS CB pdu", e); return null; } } /** * Holds all info about a message page needed to assemble a complete concatenated message. */ private static final class SmsCbConcatInfo { private final SmsCbHeader mHeader; private final SmsCbLocation mLocation; 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); } } }