Как работает умный обработчик служебных смс (показывает только важную информацию)

в 5:34, , рубрики: android, java, sms, разработка мобильных приложений, Разработка под android

Данная статья содержит описание внутреннего устройства умного обработчика служебных смс.
Приложение парсит входящие смс-ки и показывает только важную информацию из них.
Показывает красиво, быстро и удобно.

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);

}

После этого пользователь видит такой экран
Смысл в том, чтобы быстро увидеть полезную информацию

image

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

Но поставленную задачу приложние решает.

image

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

Автор: pavel_ismailov

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js