Commit b2794460 authored by Evan W. Patton's avatar Evan W. Patton

Make SmsBroadcastReceiver more robust

Josh ran into a NullPointerException when receiving a SMS message
using a Pixel running Google Fi. There are a few possible reasons this
could happen:

1. We linked against android.telephony.gms.SmsMessage. If the message
   was formatted as CDMA (3gpp2) then it is possible that the message
   returned is null, resulting in the NPE.

2. Any format other than 3gpp or 3gpp2 will result in the same flow as
   1 if a new format is introduced in the future, or if the format
   were something such as a data packet from Google Voice.

In API 19, there was a new
android.provider.Telephony.Sms.Intents#getMessagesFromIntent
introduced to abstract away extracting SmsMessages from Intents. This
commit uses this API when the SDK is 19 or higher. We also catch NPE
in the broadcast receiver and log it to Logcat to prevent crashing.

Change-Id: I9f00d6156f004d3d72938de1e57b31cfb034d241
parent f28861e2
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2017 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import android.content.Intent;
import android.provider.Telephony.Sms.Intents;
import android.telephony.SmsMessage;
/**
* Helper methods for calling APIs added in KITKAT (4.4, API level 19)
*
* @author Evan W. Patton (ewpatton@mit.edu)
*
*/
public final class KitkatUtil {
private KitkatUtil() {
}
/**
* Retrieve any SmsMessage objects encoded in the SMS_RECEIVED intent.
*
* @param intent An intent passed by Android to the SMS_RECEIVED receiver.
* @return A list of SmsMessages. The list will be non-null but zero length if the intent lacks
* SMS content.
*/
public static List<SmsMessage> getMessagesFromIntent(Intent intent) {
List<SmsMessage> result = new ArrayList<SmsMessage>();
SmsMessage[] messages = Intents.getMessagesFromIntent(intent);
if (messages != null && messages.length >= 0) {
Collections.addAll(result, messages);
}
return result;
}
}
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2017 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime.util;
import java.util.Locale;
import android.telephony.PhoneNumberUtils;
/**
* Helper methods for calling APIs added in LOLLIPOP (5.0, API Level 21)
*
* @author Evan W. Patton (ewpatton@mit.edu)
*
*/
public final class LollipopUtil {
private LollipopUtil() {
}
/**
* Format a phone number based on the number's country code, falling
* back to the format defined by the user's current locale. This is
* to replace calling {@link PhoneNumberUtils#formatNumber(String)},
* which was deprecated in the LOLLIPOP release.
*
* @see PhoneNumberUtils#formatNumber(String, String)
* @param number The phone number to be formatted
* @return The phone number, formatted based on the country code or
* user's locale.
*/
public static String formatNumber(String number) {
return PhoneNumberUtils.formatNumber(number, Locale.getDefault().getCountry());
}
}
......@@ -31,6 +31,8 @@ public class SdkLevel {
public static final int LEVEL_JELLYBEAN = 16; // a.k.a. 4.1
public static final int LEVEL_JELLYBEAN_MR1 = 17; // a.k.a. 4.2
public static final int LEVEL_JELLYBEAN_MR2 = 18; // a.k.a. 4.3
public static final int LEVEL_KITKAT = 19; // a.k.a. 4.4
public static final int LEVEL_LOLLIPOP = 21; // a.k.a. 5.0
private SdkLevel() {
}
......
......@@ -10,6 +10,8 @@
*/
package com.google.appinventor.components.runtime.util;
import java.util.List;
import com.google.appinventor.components.common.ComponentConstants;
import com.google.appinventor.components.runtime.Texting;
import com.google.appinventor.components.runtime.ReplForm;
......@@ -22,7 +24,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.PhoneNumberUtils;
import android.telephony.gsm.SmsMessage;
import android.telephony.SmsMessage;
import android.util.Log;
/**
......@@ -114,21 +116,39 @@ public class SmsBroadcastReceiver extends BroadcastReceiver {
private String getPhoneNumber(Intent intent) {
String phone = "";
// For Google Voice, phone and msg are stored in String extras. Pretty them up
if (intent.getAction().equals("com.google.android.apps.googlevoice.SMS_RECEIVED")) {
phone = intent.getExtras().getString(Texting.PHONE_NUMBER_TAG);
phone = PhoneNumberUtils.formatNumber(phone);
// For Telephony, phone and msg are stored in PDUs.
try {
if (intent.getAction().equals("com.google.android.apps.googlevoice.SMS_RECEIVED")) {
// For Google Voice, phone and msg are stored in String extras. Pretty them up
} else {
Object[] pdus = (Object[]) intent.getExtras().get("pdus");
for (Object pdu : pdus) {
SmsMessage smsMsg = SmsMessage.createFromPdu((byte[]) pdu);
phone = smsMsg.getOriginatingAddress();
phone = intent.getExtras().getString(Texting.PHONE_NUMBER_TAG);
phone = PhoneNumberUtils.formatNumber(phone);
} else if (SdkLevel.getLevel() >= SdkLevel.LEVEL_KITKAT) {
// On KitKat or higher, use the convience getMessageFromIntent method.
List<SmsMessage> messages = KitkatUtil.getMessagesFromIntent(intent);
for (SmsMessage smsMsg : messages) {
if (smsMsg != null) {
// getOriginatingAddress() can throw a NPE if its wrapped message is null, but there
// isn't an API to check whether this is the case.
phone = smsMsg.getOriginatingAddress();
if (SdkLevel.getLevel() >= SdkLevel.LEVEL_LOLLIPOP) {
phone = LollipopUtil.formatNumber(phone);
} else {
phone = PhoneNumberUtils.formatNumber(phone);
}
}
}
} else {
// On SDK older than KitKat, we have to manually process the PDUs.
Object[] pdus = (Object[]) intent.getExtras().get("pdus");
for (Object pdu : pdus) {
SmsMessage smsMsg = SmsMessage.createFromPdu((byte[]) pdu);
phone = smsMsg.getOriginatingAddress();
phone = PhoneNumberUtils.formatNumber(phone);
}
}
} catch(NullPointerException e) {
Log.w(TAG, "Unable to retrieve originating address from SmsMessage", e);
}
return phone;
}
......@@ -141,19 +161,32 @@ public class SmsBroadcastReceiver extends BroadcastReceiver {
private String getMessage(Intent intent) {
String msg = "";
// For Google Voice, msg is stored in String extras.
if (intent.getAction().equals("com.google.android.apps.googlevoice.SMS_RECEIVED")) {
msg = intent.getExtras().getString(Texting.MESSAGE_TAG);
// For Telephony, phone and msg are stored in PDUs.
} else {
Object[] pdus = (Object[]) intent.getExtras().get("pdus");
for (Object pdu : pdus) {
SmsMessage smsMsg = SmsMessage.createFromPdu((byte[]) pdu);
msg = smsMsg.getMessageBody();
try {
if (intent.getAction().equals("com.google.android.apps.googlevoice.SMS_RECEIVED")) {
// For Google Voice, msg is stored in String extras.
msg = intent.getExtras().getString(Texting.MESSAGE_TAG);
} else if (SdkLevel.getLevel() >= SdkLevel.LEVEL_KITKAT) {
// On KitKat or higher, use the convience getMessageFromIntent method.
List<SmsMessage> messages = KitkatUtil.getMessagesFromIntent(intent);
for (SmsMessage smsMsg : messages) {
if (smsMsg != null) {
msg = smsMsg.getMessageBody();
}
}
} else {
// On SDK older than KitKat, we have to manually process the PDUs.
Object[] pdus = (Object[]) intent.getExtras().get("pdus");
for (Object pdu : pdus) {
SmsMessage smsMsg = SmsMessage.createFromPdu((byte[]) pdu);
msg = smsMsg.getMessageBody();
}
}
} catch(NullPointerException e) {
// getMessageBody() can throw a NPE if its wrapped message is null, but there isn't an
// API to check whether this is the case.
Log.w(TAG, "Unable to retrieve message body from SmsMessage", e);
}
return msg;
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment