blob: 9367b5dccc4689c7e1fa92c30993864421f528be [file] [log] [blame]
/*
* 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 static com.android.internal.telephony.gsm.SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER;
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.AsyncResult;
import android.os.Message;
import android.os.SystemClock;
import android.provider.Telephony.CellBroadcasts;
import android.telephony.CellLocation;
import android.telephony.SmsCbLocation;
import android.telephony.SmsCbMessage;
import android.telephony.TelephonyManager;
import android.telephony.gsm.GsmCellLocation;
import android.text.format.DateUtils;
import com.android.internal.telephony.CbGeoUtils.Geometry;
import com.android.internal.telephony.CellBroadcastHandler;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.gsm.GsmSmsCbMessage.GeoFencingTriggerMessage;
import com.android.internal.telephony.gsm.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
/**
* 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 being broadcasted. */
private static final String MESSAGE_NOT_BROADCASTED = "0";
/** This map holds incomplete concatenated messages waiting for assembly. */
private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
new HashMap<SmsCbConcatInfo, byte[][]>(4);
private long mLastAirplaneModeTime = 0;
protected GsmCellBroadcastHandler(Context context, Phone phone) {
super("GsmCellBroadcastHandler", context, phone);
phone.mCi.setOnNewGsmBroadcastSms(getHandler(), EVENT_NEW_SMS_MESSAGE, null);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
mContext.registerReceiver(
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Intent.ACTION_AIRPLANE_MODE_CHANGED:
boolean airplaneModeOn = intent.getBooleanExtra("state", false);
if (airplaneModeOn) {
mLastAirplaneModeTime = System.currentTimeMillis();
}
break;
}
}
}, intentFilter);
}
@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,
Phone phone) {
GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, phone);
handler.start();
return handler;
}
/**
* 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) {
final List<SmsCbMessage> cbMessages = new ArrayList<>();
final List<Uri> cbMessageUris = new ArrayList<>();
long lastReceivedTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS;
Resources res = mContext.getResources();
if (res.getBoolean(com.android.internal.R.bool.reset_geo_fencing_check_after_boot_or_apm)) {
// Check last airplane mode time
lastReceivedTime = Long.max(lastReceivedTime, mLastAirplaneModeTime);
// Check last boot up time
lastReceivedTime = Long.max(lastReceivedTime,
System.currentTimeMillis() - SystemClock.elapsedRealtime());
}
// Find the cell broadcast message identify by the message identifier and serial number
// and is not broadcasted.
String where = CellBroadcasts.SERVICE_CATEGORY + "=? AND "
+ CellBroadcasts.SERIAL_NUMBER + "=? AND "
+ CellBroadcasts.MESSAGE_BROADCASTED + "=? AND "
+ CellBroadcasts.RECEIVED_TIME + ">?";
ContentResolver resolver = mContext.getContentResolver();
for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) {
try (Cursor cursor = resolver.query(CELL_BROADCAST_URI,
CellBroadcasts.QUERY_COLUMNS_FWK,
where,
new String[] { Integer.toString(identity.messageIdentifier),
Integer.toString(identity.serialNumber), MESSAGE_NOT_BROADCASTED,
Long.toString(lastReceivedTime) },
null /* sortOrder */)) {
if (cursor != null) {
while (cursor.moveToNext()) {
cbMessages.add(SmsCbMessage.createFromCursor(cursor));
cbMessageUris.add(ContentUris.withAppendedId(CELL_BROADCAST_URI,
cursor.getInt(cursor.getColumnIndex(CellBroadcasts._ID))));
}
}
}
}
log("Found " + cbMessages.size() + " 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 maximumWaitTimeSec = 0;
for (SmsCbMessage msg : cbMessages) {
maximumWaitTimeSec = Math.max(maximumWaitTimeSec, msg.getMaximumWaitingTime());
}
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;
}
requestLocationUpdate(location -> {
if (location == null) {
// If the location is not available, broadcast the messages directly.
broadcastMessage(cbMessages, cbMessageUris);
} else {
for (int i = 0; i < cbMessages.size(); i++) {
List<Geometry> broadcastArea = !commonBroadcastArea.isEmpty()
? commonBroadcastArea : cbMessages.get(i).getGeometries();
if (broadcastArea == null || broadcastArea.isEmpty()) {
broadcastMessage(cbMessages.get(i), cbMessageUris.get(i));
} else {
performGeoFencing(cbMessages.get(i), cbMessageUris.get(i), broadcastArea,
location);
}
}
}
}, maximumWaitTimeSec);
return true;
}
/**
* 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) {
if (message.obj instanceof AsyncResult) {
SmsCbHeader header = createSmsCbHeader((AsyncResult) message.obj);
if (header == null) return false;
AsyncResult ar = (AsyncResult) message.obj;
byte[] pdu = (byte[]) ar.result;
if (header.getServiceCategory() == MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) {
GeoFencingTriggerMessage triggerMessage =
GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu);
if (triggerMessage != null) {
return handleGeoFencingTriggerMessage(triggerMessage);
}
} else {
SmsCbMessage cbMessage = handleGsmBroadcastSms(header, ar);
if (cbMessage != null) {
handleBroadcastSms(cbMessage);
return true;
}
if (VDBG) log("Not handled GSM broadcasts.");
}
}
return super.handleSmsMessage(message);
}
/**
* Handle 3GPP format SMS-CB message.
* @param header the cellbroadcast header.
* @param ar the AsyncResult containing the received PDUs
*/
private SmsCbMessage handleGsmBroadcastSms(SmsCbHeader header, 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());
}
}
if (VDBG) log("header=" + header);
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);
}
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);
} catch (RuntimeException e) {
loge("Error in decoding SMS CB pdu", e);
return null;
}
}
private SmsCbHeader createSmsCbHeader(AsyncResult ar) {
try {
return new SmsCbHeader((byte[]) ar.result);
} catch (Exception ex) {
loge("Can't create SmsCbHeader, ex = " + ex.toString());
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);
}
}
}