Данная статья содержит описание внутреннего устройства умного обработчика служебных смс.
Приложение парсит входящие смс-ки и показывает только важную информацию из них.
Показывает красиво, быстро и удобно.
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 для каждого отправителя)
- умный алгоритм прогона по шаблонам (сначала все с кодами — они нужны быстрее всего, затем часто используемые, затем все остальные)
- вероятно, можно улучшить сам код парсинга (проверить на создание лишних объектов, уменьшить количество циклов и прочее)
Но поставленную задачу приложние решает.

Прилагаю главный класс для парсинга сообщений.
Он немного отличается от кода, приведенного выше,
т.к. приведенный код был улучшен визуально.
