Pull to refresh

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

Reading time9 min
Views7.5K

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


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


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

Tags:
Hubs:
+9
Comments11

Articles

Change theme settings