Привет! Сегодня я хочу затронуть тему 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. Буду рад исправлениям/пожеланиям. На сей ноте смею откланяться, до новых встреч на просторе Хабра!