Данная статья содержит описание внутреннего устройства умного обработчика служебных смс.
Приложение парсит входящие смс-ки и показывает только важную информацию из них.
Показывает красиво, быстро и удобно.
1. Как это работает
В манифесте прописываем разрешение на получение и чтение SMS
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.READ_SMS"/>`
Там же регистрируем receiver
Разрешение action_sms_received_test
нужно для тестирования.
Чтобы не тратить деньги на настоящие смс во время тестирования, я отправляю Intent с этим action из приложения и ловлю его.
<receiver android:name=".receivers.SmsReceiver">
<intent-filter android:priority="2147483647">
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
<action android:name="action_sms_received_test"/>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
Теперь ресивер будет получать все входящие сообщения
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case ACTION_SMS_RECEIVED:
handleIncomingSms(context, intent);
break;
case ACTION_SMS_RECEIVED_TEST:
// do test
break;
}
}
Теперь в методе handleIncomingSms(context, intent);
требуется разобраться, что за СМС нам пришла, и принять решение о том, что делать.
Если она является служебной — мы её разбираем, достаем полезную информацию, и отображаем её в красивом виде.
Каким образом мы понимаем, служебная она или нет — опишу позже.
Грубо, это выглядит так
private void handleIncomingSms(Context context, Intent intent) {
L.i("handleIncomingSms");
Bundle bundle = intent.getExtras();
if (bundle == null) {
return;
}
try {
Object[] pdus = (Object[]) bundle.get(PDUS);
String smsText = "";
for (Object pdu : pdus) {
final SmsMessage message = SmsMessage.createFromPdu((byte[]) pdu);
smsText += message.getMessageBody();
}
checkTemplates(context, smsText);
} catch (Exception e) {
L.i("handleIncomingSms - Exception", Log.getStackTraceString(e));
}
}
Метод checkTemplates();
private void checkTemplates(Context context, String smsText) {
L.i("checkTemplates", smsText);
// get templates
List<SmsTemplate> smsTemplates = DatabaseManager.getSmsTemplates();
if (smsTemplates == null) {
return;
}
// check if sms text according to some template
for (SmsTemplate smsTemplate : smsTemplates) {
List<String> messageLines = SmsNewParser.getMessageLines(smsTemplate, smsText);
if (messageLines != null) {
Sender sender = DatabaseManager.getSender(smsTemplate.sender);
showPopupDialog(context, messageLines, sender != null ? sender.iconUrl : "");
}
}
}
Метод showPopupDialog
private void showPopupDialog(Context context, List<String> message, String iconUrl) {
L.i("showPopupDialog", message, iconUrl);
Intent popupIntent = new Intent(context, PopupActivity.class);
popupIntent.putExtra(PopupActivity.ICON_URL, iconUrl);
popupIntent.putExtra(PopupActivity.MESSAGE_0, message.get(0));
popupIntent.putExtra(PopupActivity.MESSAGE_1, message.get(1));
popupIntent.putExtra(PopupActivity.MESSAGE_2, message.get(2));
popupIntent.putExtra(PopupActivity.MESSAGE_3, message.get(3));
popupIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(popupIntent);
}
После этого пользователь видит такой экран
Смысл в том, чтобы быстро увидеть полезную информацию
2. Алгоритм распознавания СМС и выдачи важной информации
2.1. Кратко
- На сервере есть шаблоны
- В каждом шаблоне указано а) как должна выглядеть СМС б) что именно показывать для неё
- Приложение при каждом запуске синхронизирует их
- Каждое входящее сообщение прогоняется по всем шаблонам
- Если найден шаблон, которому она соответствует — показывается важная информация в нужной форме
2.2. Подробно о модели
Шаблон выглядит так
{
"sender": "bank_alfa",
"text": "3*8272; Pokupka; Uspeshno; Summa: 212,30 RUR; Ostatok: 20537,96 RUR; RU/MOSKVA/GETT; 15.04.2016 06:02:43",
"mask": "~N~*~N4~; ~BANK_ACTION_0~; Uspeshno; Summa: ~SUM_0~ ~CURRENCY_0~; ~BANK_ACTION_1~: ~SUM_1~ ~CURRENCY_1~; ~WORD~; ~N2~.~N2~.~N4~ ~N2~:~N2~:~N2~",
"lines": [
{
"line": "EXTRA_PURCHASE"
},
{
"line": "SUM_0"
},
{
"line": "EXTRA_TOTAL"
},
{
"line": "SUM_1"
}
]
}
sender
— отправительtext
— начальный текст настоящей смс. может быть использован для тестовmask
— сам шаблон. используются служебные слова вида~FOO~
lines
— строки сообщения, которое будет выдаваться на экран. В них можно указывать части шаблона, а можно использовать слова, которых нет в шаблоне.
Служебные слова делятся на extra
и обычные.
Extra
означает, что их нет в шаблоне.
Примеры:
~SUM~
— обычное служебное слово. Означает выражение с цифрами, разделенное точкой или запятой.
Используется для определения суммы денег. Для его поиска используется regex
{
"name": "SUM",
"regex": "\d+[.,]{0,1}\d+",
"values": [],
"is_extra": false
}
~CURRENCY~
— обычное слово, которое может принимать несколько значений. Для его поиска используется перебор его значений.
{
"name": "CURRENCY",
"regex": "",
"values": [
{
"value": "usd"
},
{
"value": "rur"
},
{
"value": "eur"
},
{
"value": "rub"
}
],
"is_extra": false
}
~EXTRA_CODE_WORD~
— служебное слово типа extra
. Используется для вывода текста "Кодовое слово" в результате.
{
"name": "EXTRA_CODE_WORD",
"regex": "",
"values": [
{
"value": "Кодовое слово"
}
],
"is_extra": true
}
также нам нужны картинки, чтобы показать, кто именно отправил сообщение.
Эта информация хранится в объектах sender
.
Пример:
Это Альфа банк и его иконка.
{
name: "bank_alfa",
icon_url: "https://dl.dropboxusercontent.com/u/1816879/CaptainSms/logo_alfa.png"
}
В итоге не сервере хранится
- Шаблоны
- Служебные слова
- Отправители
Полный json можно посмотреть здесь
2.3. Подробно об алгоритме
Мы скачиваем модель, сохраняем её.
Дальше следует сама процедура разбора смс и создание результирующего сообщения.
Для парсинга текста сообщения я использую класс SmsParser
со статичными методами.
Главный метод — getMessageLines(SmsTemplate smsTemplate, String realSmsText)
Он возвращает строки сообщения, если все ок, или null
, если мы не нашли подходящий шаблон.
Этот метод вызывается из этого места метода checkTemplates
, приведенного выше.
// check if sms text according to some template
for (SmsTemplate smsTemplate : smsTemplates) {
List<String> messageLines = SmsNewParser.getMessageLines(smsTemplate, smsText);
if (messageLines != null) {
Sender sender = DatabaseManager.getSender(smsTemplate.sender);
showPopupDialog(context, messageLines, sender != null ? sender.iconUrl : "");
}
}
Мы проходим по всем шаблонам из базы и пытаемся для каждого взять message lines
.
Если получилось — показываем экран с информацией..
Логика getMessageLines
кратко
Бежим по маске и сравниваем её посимвольно с текстом смс, записывая в массив значения встретившихся служебных слов, или выкидывая null
если встретили несоответствия
Логика getMessageLines
подробнее:
- Бежим посимвольно по тексту маски
- Если символ — это начало служебного слова (
~
), то:
— Понимаем, что это за слово (например, ~SUM_0~)
— Вычисляем его значение в тексте СМС (например,255.00
)
— Отрезаем от маски это слово, а от текста это значение (чтобы дальше бежать посимвольно) - Иначе, если это простой символ, то:
— Если они совпадают в максе и тексте, то отрезаем их оттуда и оттуда чтобы дальше сравнивать
— Если они разные, то выкидываемnull
— текст не подходит под шаблон
Логика с примерами кода
Как параметры, в метод нам приходят шаблон и текст смс
public static List<String> getMessageLines(SmsTemplate smsTemplate, String smsText)
В начале метода инициализируем лист служебных слов. В базу они попали из регулярного обновления с апи.
Нам нужна глобальная переменная, т.к. метод большой и разбит на части.
private static void initReservedWords() {
L.i("initReservedWords");
mReservedWords.clear();
mReservedWords = DatabaseManager.getReservedWords();
}
Затем создаем список служебных слов из заданного шаблона.
List<ReservedWord> reservedWords = new ArrayList<>();
for (SmsTemplateLine line : smsTemplate.lines) {
reservedWords.add(getReservedWordByName(line.line));
}
т.е. если у нас есть шаблон
{
"sender": "bank_alfa",
"text": "3*8272; Pokupka; Uspeshno; Summa: 212,30 RUR; Ostatok: 20537,96 RUR; RU/MOSKVA/GETT; 15.04.2016 06:02:43",
"mask": "~N~*~N4~; ~BANK_ACTION_0~; Uspeshno; Summa: ~SUM_0~ ~CURRENCY_0~; ~BANK_ACTION_1~: ~SUM_1~ ~CURRENCY_1~; ~WORD~; ~N2~.~N2~.~N4~ ~N2~:~N2~:~N2~",
"lines": [
{
"line": "EXTRA_PURCHASE"
},
{
"line": "SUM_0"
},
{
"line": "EXTRA_TOTAL"
},
{
"line": "SUM_1"
}
]
}
то мы хотим получить список
- EXTRA_PURCHASE
- SUM_0
- EXTRA_TOTAL
- SUM_1
далее идет основная логика
// check match symbol by symbol
try {
do {
String s = mask.substring(0, 1);
if (s.equals(ReservedWord.SYMBOL)) {
// found start of a reserved word
ReservedWord currentReservedWord = getFirstReservedWord(mask);
String valueOfCurrentReservedWord = getValueOfReservedWord(smsText, mask, currentReservedWord);
// add value in the list, if reserved word is in the list
if (reservedWords.contains(currentReservedWord) && valueOfCurrentReservedWord.length() > 0) {
values.put(currentReservedWord.getForm(), valueOfCurrentReservedWord);
}
// cut text and mask to look next symbols
smsText = smsText.substring(valueOfCurrentReservedWord.length());
mask = mask.substring(currentReservedWord.getForm().length());
} else if (s.equals(smsText.substring(0, 1))) {
// that symbols matches, go to the next symbol
smsText = smsText.substring(1);
mask = mask.substring(1);
} else {
/*
* that symbol does not match, so text not match that mask, so method fails
* because we cannot return correct values according to that list of reserved word
*/
return null;
}
} while (mask.length() > 0);
} catch (StringIndexOutOfBoundsException e) {
/*
* There is some error during parsing.
* That mean text does not match mask.
*/
L.i(TAG, "getMessageLines - Exception - " + Log.getStackTraceString(e));
return null;
}
Она делает ровно то, что описано выше, как "Логика getMessageLines
подробнее:"
Далее мы пересортировываем список, т.к. в тексте он встречается в другом порядке, чем наших message lines
// convert list to the right order
List<String> valuesList = new ArrayList<>();
for (ReservedWord word : reservedWords) {
LLog.e(TAG, "getMessageLines - return list - " + values.get(word.getForm()));
if (values.get(word.getForm()) != null) {
valuesList.add(values.get(word.getForm()));
}
}
Далее мы добавляем служебные слова типа extra
, т.к. мы их не находили при прохождении по тексту смс.
// add values of all the extra words
for (int i = 0; i < reservedWords.size(); i++) {
if (reservedWords.get(i).isExtra) {
valuesList.add(i, reservedWords.get(i).values.iterator().next().value);
}
}
Это нужно вот почему.
На вход нам подали smsTemplate
. У него есть набор messageLines. Например, их было 4.
"lines": [
{
"line": "EXTRA_PURCHASE"
},
{
"line": "SUM_0"
},
{
"line": "EXTRA_TOTAL"
},
{
"line": "SUM_1"
}
]
}
Но в процессе проверки текста на совпадение с шаблоном мы нашли только SUM_0
и SUM_1
Т.к. это данные, которые реально есть в тексте СМС.
Таким образом, после первого куска логики мы имеем массив из двух элементов (в данном случае 212,30
и 20537,96
).
Но на выход нам нужно подать 4 строки (к этим двум нужно еще добавить EXTRA_PURCHASE
и EXTRA_TOTAL
), причем в нужном порядке.
Поэтому в конце метода мы их добавляем.
В итоге, на выходе мы получаем массив из четырех строк.
Например, если у нас был шаблон
{
"sender": "bank_alfa",
"text": "3*8272; Pokupka; Uspeshno; Summa: 212,30 RUR; Ostatok: 20537,96 RUR; RU/MOSKVA/GETT; 15.04.2016 06:02:43",
"mask": "~N~*~N4~; ~BANK_ACTION_0~; Uspeshno; Summa: ~SUM_0~ ~CURRENCY_0~; ~BANK_ACTION_1~: ~SUM_1~ ~CURRENCY_1~; ~WORD~; ~N2~.~N2~.~N4~ ~N2~:~N2~:~N2~",
"lines": [
{
"line": "EXTRA_PURCHASE"
},
{
"line": "SUM_0"
},
{
"line": "EXTRA_TOTAL"
},
{
"line": "SUM_1"
}
]
}
то на выходе мы получим
- Покупка
- 212,30
- Осталось
- 20537,96
На этом главная логика заканчивается.
Далее мы просто показываем это в нашей попап активити таким методом
showPopupDialog(context, messageLines, sender != null ? sender.iconUrl : "");
Текст messageLines
просто отображается в текст вьюшках.
iconUrl
подгружается в image view с помощью Glide — тут все предельно просто.
Заключение
Очевидно, что алгоритм примитивен и может быть улучшен.
Из идей
- разбить api на разные json файлы (например один json для каждого отправителя)
- умный алгоритм прогона по шаблонам (сначала все с кодами — они нужны быстрее всего, затем часто используемые, затем все остальные)
- вероятно, можно улучшить сам код парсинга (проверить на создание лишних объектов, уменьшить количество циклов и прочее)
Но поставленную задачу приложние решает.
Прилагаю главный класс для парсинга сообщений.
Он немного отличается от кода, приведенного выше,
т.к. приведенный код был улучшен визуально.
Автор: pavel_ismailov