На твой телефон пришло новое сообщение +5


Привет! Сегодня я хочу затронуть тему SMS, а точнее, поделиться опытом их «приручения» в Android на примере собственного пет-проекта.

Интересный факт: в этом году, а точнее, 3 декабря, будет ровно 30 лет, как было отправлено первое SMS. За это время мы неплохо так шагнули в своем развитии, перешли от кнопочных телефонов к смартфонам, научились пользоваться мессенджерами. Но SMS-ки всё ещё здесь и даже как будто бы не планируют никуда уходить: экстренные оповещения, проверочные коды 2FA, промокоды на пиццу или тот же SMS-банкинг, как пример. Вот, кстати, о последнем и пойдёт речь.

Немного предыстории

Перед тем, как перейти к основной части статьи, с вашего позволения, я бы хотел ещё немного поболтать и рассказать, что послужило основой для этой статьи. У меня есть две черты. Первая: я обожаю цифровизацию. Мне нравится, что смартфоны заменяют нам и записную книжку, и калькулятор, и кучу других полезных повседневных «гаджетов». Но при всём при этом я очень избирательно подхожу к выбору приложений. И всё дело здесь во второй моей черте: я обожаю минимализм. Как ни странно бы это звучало, иногда для меня широкий функционал может стать не стимулом к установке, а прямо наоборот – поводом, чтобы пройти мимо. Часто я хочу точный узконаправленный инструмент, без, так скажем, «шума». В итоге, суммарно эти две черты сподвигают меня на создание собственного, максимально настроенного «под себя» продукта, даже не смотря на множество уже существующих. Как говорится, «потому что могу». Это вполне попадает под термин «велосипед», но благодаря подобным пробам пера я могу изучить что-нибудь новое или закрыть те или иные пробелы. Так у меня появилось собственное приложение для заметок и так появилось приложение, опыт работы над которым и послужил поводом для этой статьи.

Одним летним вечером задумались мы с женой над тем, что было бы здорово начать контролировать семейный бюджет. Не про ограничения речь, а о статистике: на что мы тратим больше всего, сколько уходит на еду и вот это вот всё. У неё к тому моменту уже было установлено приложение, да только с ним не сложилось: очень скоро мы про него забыли и забили. Несмотря на большую популярность и множество кнопочек-крутилок, нас оно не зацепило. Тогда и появилась идея для очередного собственного творения, получившего гордое название Moneytor. За пару дней удалось собрать первую рабочую версию и проверить её в реальных условиях.

Как оказалось, добавлять расходы вручную – самый базовый и часто встречающийся функционал приложений такого типа – это не самый удобный способ. У тебя приходят SMS, а ты руками «вбиваешь» каждую чашку кофе. И я подумал: а почему бы не подружить приложение с SMS?

От слов к коду

Дано: SMS-сообщения от банка о расходах по карте. Внутри разная полезная информация в формате «Где? Когда? Сколько?», из которой нас интересует лишь «Сколько?». Перед тем, как выудить столь желанные для нас данные, их стоило бы сначала получить.

Чтобы приложение было способно получать SMS-сообщения, отправляемся в AndroidManifest.xml и добавляем нужные разрешения:

<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />

К ним мы ещё вернёмся. И пока мы не ушли дальше, объявляем BroadcastReceiver, который и будет отвечать за получение SMS:

<receiver
    android:name=".core.sms.SmsReceiver"
    android:enabled="true"
    android:exported="true"
    android:permission="android.permission.BROADCAST_SMS">
    <intent-filter
        android:priority="1000">
        <action
            android:name="android.provider.Telephony.SMS_RECEIVED" />
    </intent-filter>
</receiver>

Важный момент здесь – приоритет. Мы хотим получать и обрабатывать сообщения как можно раньше (сразу), поэтому выставляем приоритет 1000, который в системе является самым высоким. Официальная документация настоятельно рекомендует использовать значения меньше 1000, но мы пойдем против и поставим 1000. Как позже окажется на практике, данный приоритет в некоторых случаях позволит получать SMS-ки раньше, чем дефолтный SMS-обработчик.

Мы почти готовы пропускать SMS-трафик через наше приложение, осталось только запросить на это разрешение. Так как разрешения входят в одну группу, мы запрашиваем их пачкой, при этом у пользователя отобразится лишь один диалог.

Когда пользователь даст согласие на получение и чтение SMS, мы, наконец, сможем прочитать их в нашем SmsReceiver. Для этого нам нужно расшифровать и склеить наше сообщение в onReceive:

override fun onReceive(context: Context?, intent: Intent?) {
    if (intent?.action != SMS_RECEIVED_ACTION) {
        return
    }

    val extras = intent.extras ?: return

    val pdus = (extras["pdus"] as Array<*>)
        .takeIf { it.isNotEmpty() }
        ?: return
    val format = extras.getString("format").orEmpty()

    val message = pdus
        .filterNotNull()
        .filterIsInstance<ByteArray>()
        .map {
            SmsMessage.createFromPdu(it, format)
        }
        .joinToString(
            separator = "",
            transform = {
                it.messageBody
            }
        )
}

В результате мы имеем текст нашего сообщения. Всё, что остаётся - вытащить значение расхода и сохранить его. В первую очередь, стоит определиться, где мы можем это сделать. Самый простой, но в корне неверный вариант – делать всё прямо в нашем ресивере. Неверный он потому, что ресивер не предназначен для выполнения длительных задач. И хотя операция по парсингу и сохранению значения не занимает много времени, выполнять её в ресивере – плохой тон.

Хорошо, если не BrodcastReceiver, то что? Фоновая задача, потенциально длительная… Service, скажете вы и будете совершенно правы. Foreground Service нас бы прекрасно выручил, если бы не одно НО: начиная с Android 12, мы больше не можем запускать сервисы, когда приложение находится в фоне. К счастью, у нас есть инструмент, который прекрасно дружит с ограничениями ОС и длительной фоновой работой одновременно – WorkManager. Как гласит документация, он учитывает механизмы работы ОС и способен выполнить нашу задачу если не сразу, то, как минимум, при первой возможности. А ещё он не требует показывать уведомления, как Foreground Service, и ко всему прочему поддерживает корутины. Я бы сказал, идеальный инструмент.

Для того, чтобы воспользоваться WorkManager, для начала нужно создать нашего Работника, который и сделает всю грязную фоновую работу за нас. Он может выглядеть примерно вот так:

class SmsProcessingWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        val message = inputData.getString(EXTRA_MESSAGE)
        val schema = settings[SettingsKeys.SMS_EXPENSES_SCHEMA]
        val expenseValue = parseExpenseValueFromMessage(message, schema)
        saveExpense(expenseValue, message)
        return Result.success()
    }

}

Worker выполняет две задачи: собственно извлечение нашего значения и его сохранение в БД. Извлечение происходит с помощью схемы, указанной пользователем. По своей сути схема – это строка, которая буквально содержит слова/буквы перед значением (числом) расхода, само значение и слов/буквы после. Зная паттерн числа, мы легко можем найти в схеме число, а за ним уже и начало, и конец строки схемы. Зная начало строки и её конец, мы просто формируем полный паттерн, который и будет использован для поиска значения расхода в тексте сообщения.

private fun parseExpenseValueFromMessage(message: String, schema: String): Float? {
    if (message.isBlank() || schema.isBlank()) {
        return null
    }

    val numberRegexString = "\\d+(.\\d+)?"
    val numberRegex = numberRegexString.toRegex()

    val numberInSchema = numberRegex.find(schema)?.value
    if (numberInSchema != null) {
        val parts = schema.split(numberInSchema)
        val start = parts.getOrNull(0)
        val end = parts.getOrNull(1)
        if (start != null && end != null) {
            val messageRegex = ("$start$numberRegexString$end").toRegex()
            val rowWithExpense = messageRegex.find(message)?.value
            if (rowWithExpense != null) {
                return numberRegex
                    .find(rowWithExpense)?.value
                    ?.toFloatOrNull()
            }
        }
    }

    return null
}

В самом приложении у пользователя есть поле, которое открывается после включения функции чтения SMS:

Остаётся лишь добавить запрос для WorkManager на выполнение работы:

context?.let {
    val inputData = SmsProcessingWorker.createInputData(message)

    val smsProcessingRequest = OneTimeWorkRequestBuilder<SmsProcessingWorker>()
        .setInputData(inputData)
        .build()

    WorkManager.getInstance(it.applicationContext)
        .enqueue(smsProcessingRequest)
}

В Moneytor каждый расход привязывается к категории. Тут возникает вопрос: как понять категорию из сообщения? Вообще, мы могли бы добавить кодовые слова для категорий, тем самым точно так же, как и со значением расхода, парсить и определять категорию. Но я поступил для себя проще: в приложении есть специальная категория SMS, к которой и привязываются подобные расходы. И так как текст сообщения записывается в описание расхода, я уже самостоятельно могу зайти и по тексту сообщения понять, к какой категории относится расход, и, соответственно, выбрать эту категорию. Для меня было главное получить автоматическую запись расходов, чтобы их не упускать и не забывать. Выставить же вручную нужную категорию можно и в конце месяца при итоговом подсчёте.

Ох уж этот зоопарк девайсов

В первую очередь работоспособность идеи я тестировал на эмуляторе. Здесь можно легко и, главное, бесплатно отправить десяток-другой тестовых SMS-ок. Получив положительный результат, я отправился ставить версию на реальный девайс, и здесь меня ждало несколько сюрпризов.

В первую очередь я установил приложение на свой Google Pixel 5a и попытался отправить сообщение с телефона жены. К моему большому удивлению, запись о расходе не появилась. Я погрузился в раздумья. Однако уже вечером после оплаты картой и получения SMS от банка, запись появилась в приложении. «Круто!», подумал я, обрадовавшись, что всё-таки код работает, однако мне всё ещё оставалось понять, почему же не сработало моё тестовое сообщение. Немного покопавшись в настройках гугловского SMS-приложения, я нашёл один занимательный переключатель:

Оказалось, что по умолчанию SMS-сообщения вовсе и не SMS, а сообщения как в самом обычном мессенджере. То есть при наличии интернета, приходят не SMS-ки, а другие сообщения, соответственно, наш бродкаст не срабатывает. Включив эту функцию, мы получаем SMS-сообщения в чистом виде. Так, мне удалось получить запись о расходе после отправки тестового сообщения.

Наладив работу приложения на своём девайсе, приступил к девайсу жены – Xiaomi Mi 11 Lite. И снова засада: при аналогичной настройке, записи не появляются. Ни от банка, ни тестовые. Открыл приложение – получил. Закрыл – ничего не приходит. С подобным поведением китайских девайсов я уже однажды сталкивался, когда работал с AlarmManager. Тестовый Huawei убивал все будильники, как только приложение отправлялось в фон. Тогда проблема оказалась в Battery Saver. Поэтому, недолго думая, отправляемся в настройки и восстанавливаем работоспособность:

1.    Отключаем ограничения Saver-а для нашего приложения

2.    Включаем автозапуск

С первым, вроде, разобрались, а что такое автозапуск? Само название сбивает с толку, и почему китайцы его так назвали, остаётся известно только китайцам.  Идея же у него следующая. В чистом Android, даже после полного уничтожения приложения (из запущенных), мы всё равно можем получать бродкасты и реагировать на них. Ребята же из Xiaomi сделали так, что, если так называемый автозапуск выключен, после выключения приложение не способно обрабатывать бродкасты, будто его и не существует вовсе. Тут стоит отметить, что есть ряд «особых» приложений (например, Facebook), у которых эта настройка всегда включена, и они работают так же, как на чистом Android без ограничений. С одной стороны, мы видим якобы заботу о батарее, с другой – отличный способ заработать. Заплатил – получил возможность работать без ограничений по умолчанию. Ведь объяснить пользователю, что нужно сделать, чтобы у него всё работало, – задача не тривиальная. А с учётом существующего букета девайсов, страшно подумать, какая длинная может получиться инструкция.

По итогу, после всех вышеописанных манипуляций, задуманный функционал заработал и на китайце. Мораль здесь простая – при работе с SMS на кастомной оболочке в первую очередь отключаем все «оптимизаторы», а уже после копаемся в коде. Ну и про инструкцию для пользователей не забываем.

Пару слов о Play Market

Если вы написали приложение, работающее с SMS, и желаете поделиться с миром, вам стоит помнить, что сами по себе разрешения для доступа к SMS считаются «особо опасными». По этой причине вам будет необходимо объяснить Google, с какой целью вам нужны SMS-ки.

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

На сегодня всё

Надеюсь, я не сильно утомил вас, и мой опыт окажется полезен. Если интересно протестировать Moneytor и его SMS-функционал, вы можете найти его на Play Market. Буду рад исправлениям/пожеланиям. На сей ноте смею откланяться, до новых встреч на просторе Хабра!




Комментарии (11):

  1. Danbka
    /#24777014 / +1

    Извините, но это приложение, как и дюжина других приложений не решает, и не может решить главную (во всяком случае для меня) проблему: категоризация расходов (продукты, бытовая химия, подарки и т.д.). Какой толк от цифры, которую вы получите в конце месяца? Её можно получить и в стандартном банковском приложении.

    Есть приложения, которые пытаются решить проблему категоризации, основываясь на названии продаца (обычно есть в смс), но не полностью, т.к. в гипермаркетах можно закупиться чем угодно.

    • FSA
      /#24777252

      Это просто один из примеров использования данного функционала. А то, что вы просите нельзя толком сделать даже получая чеки из ФНС, которые без СМС можно получить. Даже по всей той информации нужно руками разгребать позиции чеков. Я просто подобным баловался и у меня есть небольшая БД.

    • lex_u
      /#24777808

      Честно говоря, о разделении позиций самого чека в автоматическом режиме я даже и не задумывался: обычно у нас получается, что чеки под разные категории раздельные (есть категории "Продукты", "Подарки", "Для дома"). Но сама по себе идея на поразмышлять интересная, спасибо!


      Текущая реализация – это, скорее, автоматизация получения значения расхода, а вот определение категории остаётся на пользователе. То есть я убираю сам процесс переноса данных из SMS, при этом всё ещё остаётся необходимость выставления правильной категории. В то же время список категорий не ограничен рядом самых общих (как, например, в банковском приложении): вы можете добавить любые узконаправленные категории, и в любом количестве.


      Пока есть планы по добавлению к категории ключевых слов, которые позволят автоматически определить категорию для расходов из заведомо известных точек. Причём пользователь именно сам сможет указать ключевые слова для нужных ему категорий, тем самым частично решив вопрос автоматической категоризации расходов. Так, например, с учётом того, что на заправках я покупаю только топливо, в категорию "Топливо" будут записываться все расходы с ключевым словом "АЗС".

    • IvanPetrof
      /#24778016

      Пользуюсь готовым приложением, которое сканирует qr-код с чека и получает детализацию покупок из фнс (или куда там всё сливается?)
      всё работает на автомате с группировкой по категориям.

      Заголовок спойлера
      image

      в категории можно проваливаться и смотреть сколько куплено молока, хлеба и т.п. Вплоть до названия и цены

      • GayRabbit
        /#24778114

        Здравствуйте, Иван! Как называется приложение с вашего скрина? Спасибо

  2. Neo5
    /#24778058

    А Дзен-мани не пробовали? Там всё это есть - и обработка сообщений, и категоризация и аналитика и много чего ещё.

    • lex_u
      /#24780188

      Когда смотрел готовые решения, оно оказалось одним из самых подходящих, но были два НО:
      - Платная версия. Как я понял, категории как раз попадают под платный функционал.
      - Достаточно сложный путь для получения и сохранения расхода. Вход в аккаунт (скорее всего, для синхронизации, но считаю, что это должна быть дополнительная функция), заданные банковские шаблоны (стоит отдать должное, банков много, но что, если мне нужна другая схема, которой нет в списке) сходу смутили.

      Вообще, если бы нужно было приложение здесь и сейчас, думаю, Дзенмани один из лучших вариантов (по крайней мере, из тех, что я встречал). Но мне была интересна сама работа c SMS, поэтому и взялся за свой велосипед :)

  3. SensDj
    /#24778092 / +1

    А может ВЫ сможете сделать программу, которая при получении определённого СМС прибавляет громкость и отключает "Бесшумный режим" ? Бывает пытаешься дозвониться до жены по срочному вопросу, а у неё громкость убавлена на ноль.    Мне один программист сделал такую за 1000 рублей, но она много весит (19 Мб) и при перезагрузке телефона её надо вручную запускать, а жена про это забывает.