Offline-first Android finance app that reads bank SMS messages and automatically tracks your spending — zero manual input required.
| Feature | Details |
|---|---|
| Auto-import | Reads SMS from 13 banks (12 RU + МБанк KG); imports last 90 days on request; SMS is opt-in — a fresh install never reads messages until you say so |
| Push capture | PushNotificationListener captures bank app notifications in real-time alongside SMS; reads every text extra (title/text/bigText/subText/summary/info/inbox lines/ticker) so a balance line in any field is seen |
| Автопереподключение чтения пушей | Разрешение на доступ к уведомлениям и работающая служба — разные вещи: Android рвёт привязку при каждом обновлении APK и при перезагрузке, а разрешение остаётся, и экран настроек уверенно пишет «обрабатываются». Приложение чинит привязку само — после обновления и перезагрузки, плюс ежечасная мягкая проверка; если система сбросила само разрешение, приходит уведомление со ссылкой в нужный экран настроек |
| Real-time | New transactions appear instantly via SmsReceiver BroadcastReceiver (with goAsync()); cross-channel SMS↔push dedup within ±5 min |
| Marketing filter | PromoFilter drops credit-card offers and cashback ads before any parser runs — a "лимит 163 000 ₽" promo is never booked as a transfer |
| Smart categorization | Deterministic dictionary classifier (~216 merchant rules across 18 categories incl. transit, marketplaces, bookmakers, subscriptions, income); optional pre-trained TFLite ML layer (inference-only, no on-device learning). Rules always win over the model — it is frozen at 13 labels and cannot name a category added later |
| Subscriptions category | Netflix, Яндекс Плюс, Кинопоиск, СберПрайм, YouTube, iCloud, Adobe и ещё три десятка сервисов идут в отдельную категорию «Подписки», а не в «Развлечения»: билет в кино — разовая покупка, ежемесячное списание — совсем другая строка бюджета |
| Account linking | Card mask from SMS/push (e.g. ··2548) auto-links transactions to the correct account; the bank's «Остаток» is applied as an authoritative snapshot, with a recency guard so a fresher manual edit is never reverted |
| Credit cards | AccountKind.CREDIT — the balance is a debt, never mixed into net worth. Dashboard tile (free limit + debt + deadline) opens a dedicated screen: payment amount and date, interest-free period bar, rate, utilisation, repayment, per-card history |
| Credit push parsing | On a credit card Сбер prints the free limit under the same «Баланс» label a debit card uses — only the account kind can tell them apart, so it is converted to a debt using the card's limit. The «Внесите платёж … до …» reminder is filed as a fact about the card, not an operation, and the bank's own figure outranks anything the app infers |
| Repayment | «Погасить» books a TRANSFER off one of your accounts, never an expense (the purchases were already counted). Two rows, one per account, so deleting either undoes its own side. Money arriving on a credit card is classified as repayment, not income |
| Interest estimate | «Переплата сейчас» — an exact zero inside the interest-free period, an estimate once the deadline passes. «Если платить только минимум» simulates the real cost of carrying a balance. Both are labelled estimates; both are hidden without a rate |
| Multi-currency | RUB / USD / EUR / KGS (сом) per account and per transaction; hero shows each currency on its own line |
| Защита ввода | Смахнуть заполненную форму вниз больше нельзя молча: приложение спросит, выйти ли без сохранения. Пустая или не тронутая форма закрывается сразу — вопрос на каждое закрытие приучал бы отвечать «да» не глядя. Работает во всех формах: операция, цель, категория, бюджет, счёт, условия кредитки, погашение, обязательство |
| Выбор счёта | Двухшаговый: карточка банка → её счета с маской и остатком. Один и тот же выбор в ручной операции и в обязательстве |
| Manual entry | Add (bank→account picker, income presets, Расход/Доход/Перевод), edit, delete. Перевод пишется двумя строками с общей меткой пары — по одной на каждый счёт, поэтому удаление откатывает оба, а не только тот, на котором лежит запись |
| Правка счёта и даты операции | Пуш без реквизитов приходит без счёта — сумма есть, а откуда ушли деньги, приложение не знает. Раньше такую операцию приходилось удалять и вводить заново, теряя исходный текст и остаток банка. Теперь в карточке правятся счёт списания/зачисления (выбор банк → счёт, повторное нажатие отвязывает), вторая сторона перевода и дата. Баланс счёта сдвигается на сумму операции, кроме случая, когда банк уже прислал остаток по этому счёту позже — его цифра эти деньги уже учла. Что будет с балансом, карточка пишет до нажатия «Сохранить». Список счетов в карточке открывается свёрнутым — выбранный счёт виден в строке банка («•• 6703»), а сам список раскрывается только по нажатию |
| Фильтры операций | Одна строка: лупа-поиск (раскрывается по нажатию), меню «Тип операции» (Все / Расходы / Доходы / Переводы) и «Дата» — календарь на один день или период. Пустой список при выставленном фильтре честно пишет «Ничего не найдено», а не «Операций пока нет» |
| Чья это карта | На каждой операции видно источник — «•• 6703» в цвете банка. Заметность зависит от того, насколько источник редок среди видимых строк: карта, с которой идёт большинство покупок, подписана тихо-серым, а редкая — цветом банка. При 90 % операций с одного банка одинаковая цветная метка на всех строках превратилась бы в фон и перестала что-либо различать |
| Пополнение брокера | Деньги, ушедшие брокеру («Получатель платежа BKS Mir Investitsiy» и другие брокеры), — это перевод своих денег, а не трата: в списке «БКС · ↔ 10 000 ₽ · → брокеру» с категорией «Инвестиции» (брокер назван по-русски, хотя банк пишет «BKS Mir Investitsiy»), и они не попадают ни в траты, ни в бюджет, ни в оценку финансового здоровья. Вывод от брокера — тоже перевод, а не заработок. Брокер узнаётся по названию (БКС, Т-/Альфа-/ВТБ-/Сбер-Инвестиции, Финам, Freedom Finance, «Открытие Брокер»), а не по слову «инвестиции» — застройщик или «инвестиционный банк» тратой остаются. Уже записанные пополнения переразмечаются при обновлении и при восстановлении старой копии, если категорию у них не меняли руками. Восстановление копии больше не сбрасывает категории у операций на устройстве |
| Пуши Сбера с «игривым» заголовком | «Шикарный перекус в DODO PIZZA PERM-5 1 034 ₽» — это трата 1 034 ₽ в «DODO PIZZA PERM-5», а не 51 034 ₽ в «Шикарный перекус в …»: цифры в конце названия больше не прилипают к сумме, весёлая фраза Сбера отрезается. Оплата по СБП в магазине — покупка, «Выплата процентов + 77 ₽» — доход, «Деньги отправились в Альфа-Банк» — перевод. Уже записанная история исправляется при обновлении (кроме строк, которые вы правили руками) |
| Карты, которых нет в приложении | Операция с карты, не заведённой в приложении, больше не попадает на единственный счёт того же банка и не переписывает его баланс чужим «Остатком». На главной появляется подсказка с номерами таких карт: нажмите номер — откроется новый счёт с этим номером; добавленная карта забирает свои операции сама |
| Режим «Инвестор» | Переключатель «Кошелёк | Инвестор» в шапке главной — выпуклый ползунок, который переезжает под выбранный режим и меняет цвет: серебро — кошелёк, индиго — инвестор. В режиме инвестора — своя палитра (индиго) и свой экран: портфель, брокеры, позиции, движения денег, заявки и сделки; покупок, переводов и карт кошелька там нет. Приложение само замечает, из какого приложения приходят пуши брокера, и записывает их: пополнения, выводы (только подтверждённые — «в обработке» не считается, иначе вывод посчитался бы дважды), переводы между счетами брокера (итог они не меняют), заявки и сделки. Экран устроен как в приложении БКС: выбор счёта («Весь портфель» или один счёт), крупная сумма и результат за выбранный период — «24 часа», «Месяц» или «Всё время», кнопки «История» и «Заявки», ниже — сворачиваемые группы «Валюта / Акции / Облигации / Фонды / Внебиржевые активы». Сумма считается по цене ваших сделок, а не по рынку, поэтому может отличаться от БКС — это подписано под суммой. Предупреждение «критично низкий баланс» — янтарная карточка вверху; она закрывается сама, когда на этот счёт придут деньги, а пока она открыта, на «Инвестор» в переключателе горит точка. Всё, что приходит пушем, можно ввести и вручную — кнопка «Добавить»: операция (пополнение, вывод, перевод между счетами, покупка, продажа), актив (бумага, купленная раньше) или счёт; у бумаги можно указать текущую цену. Нажатие на любую операцию — и ручную, и из пуша — открывает карточку: все поля можно исправить (вид, сумму, валюту, тикер, количество, цену, счёт, дату) или удалить операцию; актив, заведённый вручную, правится целиком — вместе с деньгами на его покупку. Нажатие на бумагу открывает её карточку: текущая цена, сделки, из которых сложилась позиция (каждую можно открыть и исправить), и удаление актива целиком. Счёт удаляется в выборе счёта. Котировки Мосбиржи (Настройки → «Инвестиции», по умолчанию выключено): раз в сутки приложение спрашивает у Мосбиржи цены своих бумаг и курсы валют (доллар и евро — по курсу ЦБ) — портфель считается по рынку, итог показывается одной суммой в рублях, результат «за 24 часа» и «за месяц» — по рыночным ценам, размер лота — с биржи, группа бумаги — с биржи только для бумаг, которых нет во встроенном справочнике (известные фонды вроде LQDT всегда остаются в «Фондах»). Уходят только тикеры — без сумм, количества и счетов, только по HTTPS и только на iss.moex.com; биржа отдаёт цены с задержкой 15 минут, и под суммой написано, на какое время цены. Пополнение брокерского счёта склеивается с переводом из кошелька: у пополнения видно, откуда пришли деньги («из Альфа-Банк •• 1139»), а перевод брокеру, о котором брокер так и не прислал пуш, предлагается записать пополнением (или отметить «это не пополнение»). В кошельке под «Свободно» — плитка «У брокера»: сколько денег у брокера, без бумаг и сделок и не входя в «Всего». Рыночная заявка приходит от брокера без цены: она всё равно записывается, считается по последней известной цене бумаги с пометкой «≈», а точную цену можно вписать в её карточке. Деньги вводятся в рублях, долларах, юанях, евро или гонконгских долларах, с копейками и центами («0,41 $», «41,60 ¥»); остаток валюты под тикером USD000SMALL / CNY000SMALL, введённый как актив, записывается суммой в своей валюте. У режима своя нижняя панель из пяти вкладок: Операции (вся лента у брокера с фильтрами по виду и счёту), Аналитика (всё у брокера, результат за периоды, состав по группам, бумаги по результату), Портфель посередине (крупнее остальных и подсвечен), Календарь (пока — открытые предупреждения брокера; выплаты появятся с их пушами) и Счета (что пришло и ушло по каждому счёту, добавить/удалить, приложение брокера). Пока своих данных нет — пустое состояние, ручной ввод и пример по кнопке |
| Swipe-to-delete | Swipe left to reveal a red trash button, tap it to confirm — a flick alone never deletes |
| PDF import | Import bank statements (Alfa-Bank "Операции по счету" layout) via SAF |
| Financial score | 0–100 across 4 pillars; rendered as a multi-colour donut (one arc per pillar, dimmed shortfall) so a weak pillar is visible at a glance |
| Behavioral analytics | Heatmap, fatigue curve, payday effect, impulse classification, anomaly detection, subscription gaps |
| Analytics period | Месяц / 6 мес / Год / Всё время chips drive the category + daily breakdown (the health score keeps its own point-in-time window by design) |
| Category drill-down | Interactive pseudo-3D pie — tap a slice to explode it, then open every operation of that category for this and last month |
| Transfer routing | Переводы (СБП/перевод) распознаются как TRANSFER и сами засчитываются в цель по привязке к счёту / карте / слову в СМС — включая переводы, внесённые вручную: перевод НА привязанный счёт добавляется к цели, С него — вычитается. Переводы между своими счетами спариваются, и net worth не меняется |
| Budget envelopes | Monthly/weekly limits per category, dynamic color bar (green→amber→red), alerts throttled to once per budget per month, max 2/day (persisted, survives restart) |
| Savings goals | Компактные карточки с пиксель-артом по теме цели; кольцо выполнения показывает процент внутри себя и красится в цвет темы. Расчёт прямо в форме цели: сколько осталось, сколько откладывать в месяц к сроку, за сколько наберётся вашим темпом и хватает ли его. Правка суммы — вводом текущего остатка, разницу считает приложение. Привязка к счёту: цель следует за деньгами на нём — приход прибавляется, расход вычитается. Форма: иконки по девяти категориям в два столбца, «Начало накопления» и «Срок» рядом, счета чипами банк→счёт; удаление спрашивает подтверждение, выполненные цели — под сворачиваемым заголовком |
| Subscriptions | Auto-detected recurring expenses, missed-payment alerts, monthly total. Одно и то же списание, которое банк присылает под разными описаниями («ChatGPT», «VTS OPENAI *CHATGPT SUBSC»), показывается ОДНОЙ строкой: одинаковая цена у одного сервиса — это одна подписка. Две настоящие подписки на один сервис по разной цене (19,99 и 22,40) остаются двумя строками |
| Insights & narratives | 8 Russian narrative templates, CRITICAL/WARNING/INFO severity alerts |
| What-if simulator | Interactive sliders for 6/12/24-month savings projections |
| Месяц к месяцу | В самом верху вкладки «Тренды» — траты и доход по месяцам. Каждый месяц сравнивается со своим предыдущим, в том числе через границу года: январь — с декабрём. Бары наложены: яркий бар месяца стоит поверх бледного бара предыдущего, над баром — изменение в процентах (в режиме «Год» — стрелка, цифра — по касанию); рост трат — красным, рост дохода — зелёным, падение дохода — янтарным. Окно — 6 месяцев или год. Сравнивается полный месяц с полным; текущий месяц помечен незаконченным, и его изменение показано серым. Касание по месяцу раскрывает, из каких категорий сложилась разница |
| За всё время | В аналитике после «Финансового здоровья» — плитка «Всего потрачено / всего заработано». По касанию — отдельный экран: итоги за год, 2, 10, 20 лет или всё время; две нарастающие кривые на одном графике (зелёная — заработано, красная — потрачено) с шагом месяц / полгода / год / 2 года, касание показывает цифры на конец периода; разноцветные бары по годам, внутри которых видны категории, — касание раскрывает год; круговые диаграммы по категориям трат и дохода; источники — кому ушли деньги и от кого пришли, с суммой и числом операций, пересчитываются с каждой новой операцией. Переводы между своими счетами не считаются ни тратой, ни заработком; другие валюты показаны отдельно, в рубли не пересчитываются |
| Календарь и «Свободно» | Главное число экрана — не остаток и не прогноз, а «сколько можно потратить, ничего не сломав»: деньги на счетах минус незакрытые обязательства до горизонта минус ваш резерв. Горизонт по умолчанию — до следующего поступления, а не до конца месяца. Ожидаемая зарплата показывается, но НЕ прибавляется. Источники: объявленные платежи, платёж по кредитке, конец беспроцентного периода, найденные подписки, дедлайны целей |
| Платёж по кредитке, внесённый раньше срока | Банк присылает «внесите 989,84 ₽ до 30 сентября» и замолкает — подтверждения оплаты он не шлёт. Приложение само вычитает погашения, сделанные после напоминания: карта показывает «Платёж за этот период внесён», календарь перестаёт вычитать его из «Свободно», плитка на главной больше не подгоняет «платёж через 4 дня». Платёж, сделанный ДО напоминания, не засчитывается: банк уже знал о нём, когда считал свою цифру |
| Закрытие обязательств | Когда в истории появляется подходящая операция (та же валюта, то же направление, ±15 % по сумме, окно −5/+7 дней, счёт если указан), обязательство помечается оплаченным и перестаёт вычитаться из «Свободно». Сопоставление намеренно осторожное: при сомнении обязательство остаётся открытым, а любую отметку можно снять кнопкой «Отвязать» |
| Счёт, оплаченный по частям | «Телефон, интернет 2 000 ₽», закрытые двумя переводами по 550 и 1 500 ₽ в один день, больше не висят просрочкой: приложение складывает операции ОДНОГО дня, если их не больше трёх, каждая — заметная часть суммы (≥ 20 %), а их категории не противоречат друг другу |
| «оплачено» — один раз, дальше само | Кнопка «оплачено» на строке объявленного платежа открывает «Чем оплачено?»: операции рядом со сроком, можно отметить несколько (телефон и интернет отдельными переводами). Приложение запоминает, кому идёт этот платёж, и со следующего месяца закрывает его само — даже если части пришли в разные дни, а цена подписки немного выросла. Наличные и чужая карта — «Без операции»: закрывается ровно эта дата. «Отвязать» возвращает всё назад и забывает продавца отвязанной операции |
| «Не подписка» | Найденную приложением «подписку», которая на деле просто частая покупка (пироги по субботам), можно скрыть кнопкой «не подписка» — в календаре, в предложениях и на экране подписок. Она перестаёт вычитаться из «Свободно»; «Вернуть» внизу списка показывает скрытое снова |
| Savings calculator | Отдельный экран с тремя режимами: что накопится за срок, за сколько наберётся нужная сумма, сколько для этого откладывать. Капитализация (месяц/квартал/год/без), взнос в начале или конце месяца, ежегодная индексация взноса, инфляция → «в сегодняшних деньгах», НДФЛ 13 %, эффективная ставка, разбивка «своё / проценты» столбиками и таблицей по годам, и точка перелома — год, когда проценты начинают приносить больше ваших взносов. Подставляет ваш собственный темп накопления и суммы ваших целей |
| Backup / restore | Экспорт девяти наборов данных (счета, карты, категории, цели, бюджеты, привязки, плановые платежи, операции, события брокера) в файл; восстановление additive, идемпотентное и не ломающее внешние ключи. Файл НЕ зашифрован — это обычный JSON, читаемый любым текстовым редактором: ключ Android Keystore привязан к устройству, и зашифрованная копия не открывалась ровно там, где копия и нужна, — на новом телефоне и после переустановки. Храните её как документ с полной историей ваших финансов, потому что это она и есть |
| Notifications | Budget alerts, weekly summaries, critical insights, update-available (4 channels) |
| Deep-links | Notification taps navigate directly to the relevant screen (allowlisted routes) |
| Settings | Hero variant, animations/atmosphere/cat mode, budget alert threshold, biometric lock, ML toggle, SMS opt-in, categories CRUD, backup, updates |
| Biometric lock | Off by default and never prompts until the preference is confirmed; always offers a device-PIN escape hatch so a broken sensor can't lock you out |
| Shimmer & Cat mode | Optional atmosphere layer (particles, tilt/sheen cards, breathing hero, bioluminescent ripple) and a mood-matched cat mascot with paw-print particles |
| In-app update | Checks GitHub Releases, downloads and installs a newer APK; a 12 h background worker pushes a notification when one appears |
| Home-screen widget | 2×2 balance widget via AppWidgetProvider |
| Offline by design | The updater is the only networked component; no financial data ever leaves the device |
| Tier | Bank | SMS sender | Push package |
|---|---|---|---|
| P1 | Сбербанк | SBERBANK, 900 | ru.sberbankmobile, ru.sberbank.sbbol |
| P1 | Т-Банк | TINKOFF, TBANK, 2200 | ru.tinkoff.cardsnew, com.idamob.tinkoff.android |
| P1 | ВТБ | VTB | ru.vtb24.mobilebanking.android |
| P1 | Альфа-Банк | ALFABANK, ALFA | ru.alfabank.mobile.android |
| P1 | Газпромбанк | GAZPROMBANK, GPB | ru.gazprombank.android.mobilebank |
| P2 | Райффайзен | RAIFFEISEN | ru.raiffeisenmobile.android |
| P2 | Росбанк | ROSBANK | ru.rosbank.android |
| P2 | Открытие | OTKRITIE | ru.ftc.otkritie |
| P3 | МТС Банк | MTSB | ru.mtsbank.mobilebank |
| P3 | Почта Банк | POSTABANK | ru.pochtabank.android |
| P3 | Россельхозбанк | RSHB | ru.rshb.mbank |
| P3 | МКБ | MKB, МКБ, MKBMOBILE | ru.mkb.mobilebank, ru.mkb.android |
| KG | МБанк (Кыргызстан) | MBANK | com.maanavan.mb_kyrgyzstan |
Каждый банк — отдельный BankParser, подключённый одной строкой @Binds @IntoSet. Двенадцать из
тринадцати закрыты юнит-тестами; у MkbParser теста пока нет.
Имя, фирменный цвет, буква значка и ключевые слова каждого банка записаны один раз, в
core/bank/BankRegistry. Выбор банка при добавлении счёта показывает все банки из реестра (раньше
восемь; остальные можно было завести только через «Другой»). Операция Россельхозбанка без маски
карты теперь привязывается к единственному счёту этого банка, как у остальных.
- Dashboard — net worth hero (3 variants: Calm/Contrast/Minimal), current-month label, income/expense/forecast metrics, credit-card tile (free limit + debt + nearest deadline), «Свободно» tile (без обязательств — приглашение в календарь, с ними — само число), accounts with volumetric bank cards, clickable recent transactions
- Transactions — сгруппированный по дням список; одна строка фильтров — 🔍 (поиск раскрывается по нажатию), «Тип операции» (Все / Расходы / Доходы / Переводы) и «Дата» (один день или период); swipe-left-to-reveal delete, detail/edit sheet with source diagnostics, «↑ Экспорт» (CSV: дата, тип — расход / доход / перевод исходящий или входящий, сумма, валюта, получатель, категория), «↓ Импорт» (PDF)
- Analytics — period chips + 4 tabs:
- Обзор — multi-colour score donut with a per-pillar legend, expense pyramid, what-if simulator, archetype card
- Категории — interactive 3D pie (tap to explode), ТОП-3 траты, full category list; tap any category to see its operations for the SELECTED period (month, 6 months, year, all time) compared with the same stretch before it
- Тренды — daily spending curve, «Когда ты тратишь» as two tappable donuts (weekday / 4-hour bucket), «Усталость бюджета» bar chart, «Месяц к месяцу» diverging bars with
было → стало, «Импульсивность» with the actual flagged purchases. Every section has a «?» badge explaining the heuristic in plain language - Инсайты — alerts, anomalies, narratives
- Budget — envelope cards with dynamic progress bars, subscriptions button
- Goals — карточки целей с артом по теме, кольцо с процентом внутри, ряд действий (история / ± / 🔗 / удалить), «Калькулятор» по центру шапки; форма цели с категориями иконок, датами начала и срока, выбором счетов чипами
- Калькулятор — three modes over one monthly simulation; fine-tuning panel; «ваш темп» and goal chips prefill from your own data; every figure is explicitly labelled an estimate
- Календарь — «Свободно» героем с разложенной арифметикой (счета − обязательства − резерв), два режима — полоса ближайших дат и сетка месяца (точки по видам событий, выбранный день фильтрует список), список событий с пометкой источника (цифра банка / объявлено вами / найдено), подтверждение найденных подписок одним касанием, раздел «уже прошло» с возможностью отвязать
- Кредитные карты — total free limit and debt, per-card block (payment amount and date large, interest-free period bar, rate, utilisation, «Погасить»), combined history across cards
- Subscriptions — auto-detected recurring expenses, missed-payment alerts
- Settings — hero variant, customization (animations / atmosphere / cat mode), SMS opt-in + 90-day import, bank push listener, ML toggle, budget alert threshold, biometric, categories CRUD, backup/restore, updates
- Onboarding — explicit choice between "импортировать из SMS за 90 дней" and "добавлю вручную"
- Kotlin + Jetpack Compose (BOM 2024.06, Material 3, custom dark theme)
- Hilt — dependency injection with
@IntoSetmultibinding for parsers - Room 2.6.1 — local SQLite, schema v23 (22 миграции, каждая зарегистрирована в
DatabaseModule), amounts as Long kopecks (×100). Account writes use@Upsert(never@Insert(REPLACE), which would CASCADE-delete the account's cards) - DataStore — ~20 preference keys (hero variant, notifications, ML, shimmer/cat mode, SMS opt-in, budget-alert throttle state, update prefs)
- WorkManager + HiltWorkerFactory — daily analytics job + 12 h update check
- TFLite 2.14.0 — optional ML layer (graceful fallback when model files absent)
- PdfBox-Android 2.0.27.0 — statement import
- Android Keystore (AES-GCM-256) — только расшифровка старых
.fose-копий; новые пишутся открытым JSON - Clean Architecture + MVVM
app/
├── core/
│ ├── database/ # Entities, DAOs, FosDatabase (v23 — 19 categories, ~253 merchant rules, broker_events)
│ ├── parser/ # BankParser, ParserEngine, 13 bank parsers, TransferPatterns, PromoFilter, CreditNoticeParser, AmountParser, MerchantNames, ciRegex
│ ├── classifier/ # DictionaryClassifier, CategoryDefaults, CategoryClassifier interface
│ ├── sms/ # SmsReceiver (real-time), SmsReader (90-day import) — SMS only
│ ├── auth/ # BiometricHelper
│ ├── account/ # AccountLinker (card→account resolution, authoritative balance, orphan re-link)
│ ├── bank/ # BankRegistry — the one list of banks (name, colour, badge letter, link keywords)
│ ├── credit/ # CreditMath (debt, free limit, cycle, min payment, interest), CreditNoticeApplier
│ ├── transfer/ # TransferRouter (goal routing, internal pairing, counterparty leg)
│ ├── finance/ # SavingsMath (прогноз накоплений, срок до цели, требуемый взнос)
│ ├── calendar/ # CalendarEvent, PaymentDates, CalendarBuilder, FreeMoney, ObligationMatcher, ObligationSyncer
│ ├── analytics/ # AnalyticsEngine, ScoreCalculator, InsightGenerator, BehavioralAnalyzer, NarrativeEngine
│ ├── ml/ # ModelLoader, TextFeatureExtractor, MLCategoryClassifier, SpendingPredictor, BehavioralCluster
│ ├── pdf/ # PdfImporter, PdfTransactionParser
│ ├── backup/ # BackupManager (plain JSON), BackupCrypto (legacy .fose decryption only)
│ ├── update/ # UpdateChecker, UpdateCheckWorker
│ └── notifications/ # NotificationHelper (4 channels + deep-links + permission guard),
│ # PushNotificationListener, ListenerHealth, ListenerRebindReceiver,
│ # ListenerWatchdogWorker, ListenerNotice
├── data/
│ ├── repositories/ # Tx, Account, Card, Category, Budget, Goal, TransferRoute
│ └── preferences/ # UserPreferences (DataStore)
├── di/ # DatabaseModule, ParserModule, RepositoryModule, MLModule, AnalyticsModule
├── features/ # dashboard, transactions, analytics, budget, goals, calculator, calendar, subscriptions, categories, credit, onboarding, settings, auth (LockScreen)
├── navigation/ # FosNavHost, FosRoutes, bottom nav
├── widget/ # BalanceWidget
└── ui/
├── theme/ # FosColors, FosType, FosDimens, FosSurface (огранка карточек), FosTheme, FosFormatter,
│ # AmountVisualTransformation, Shimmer, BankColors, BioluminescentIndication
└── components/ # FosFormSheet (подтверждение выхода из формы), AccountPicker (банк → счёт),
# TransactionRow, FosSection (заголовки + «?»), LineChart, ScoreRing, ScoreDonut, Pie3D,
# AnalyticsCharts, WaterfallChart, SpendSeries, SpendTimeline, ForecastExplanation,
# GoalRing, GoalArt, HeatmapGrid, ExpensePyramid, WhatIfSimulator, AnimatedAmount,
# SwipeToRevealDelete, CatMascot, ParticleLayer, CurrencyReef, ShimmerCardFx
./gradlew assembleDebug
./gradlew test
./gradlew lintDebugCI (.github/workflows/android.yml) runs all three on every PR targeting dev or main.
Unit tests (app/src/test/) — 29 файлов, 364 случая:
| Suite | Coverage |
|---|---|
12 × *ParserTest |
по банку, ~7 случаев каждый (MkbParser пока не покрыт) |
GoalIconsTest |
каждая иконка цели получает подложку своей категории, глифы не повторяются |
TransferPatternsTest |
transfer detection, card-mask extraction, stem anchoring |
RealPushFormatsTest |
форматы пушей, снятые с устройства: входящий СБП, RUR, списание по счёту |
SberCreditPushTest |
покупка по кредитке и напоминание о платеже |
CyrillicCaseTest |
ciRegex против IGNORE_CASE: «ПОКУПКА» не теряется |
PromoFilterTest |
marketing pushes are dropped, real operations pass |
MerchantNamesTest |
нормализация названий продавца |
PdfTransactionParserTest |
Alfa statement layout, logical-row reconstruction |
CreditMathTest |
долг, свободный лимит, цикл, минимальный платёж, проценты |
PaymentDatesTest · FreeMoneyTest · ObligationMatcherTest |
календарь: даты, «Свободно», сопоставление, счёт по частям |
SavingsMathTest |
калькулятор накоплений (24 случая) |
BehavioralAnalyzerTest |
all 7 public methods + edge cases (28 cases) |
InsightGeneratorTest |
all 6 rules + sort order (28 cases) |
ScoreCalculatorCushionTest · SubscriptionDetectorTest · SpendTimelineTest |
подушка, подписки, полоса трат |
There are no instrumented/UI tests — screen behaviour, gestures and Compose rendering are verified by review, not automatically.
The app ships as a sideloadable APK — no Play Store needed.
Get the APK. Every push to main triggers the Release APK workflow
(.github/workflows/release-apk.yml), which builds the debug APK and publishes it as a
GitHub Release. Download FinanceOS-Hub-vX.apk from the
Releases page, open it on an
Android device, and allow installation from this source.
Self-update. Inside the app, Настройки → Обновления → Проверить обновления queries
the GitHub Releases API, compares the latest tag against the installed versionName, and —
if newer — downloads the APK and launches the system installer. This is the only feature
that uses the network (INTERNET + REQUEST_INSTALL_PACKAGES); no user data leaves the
device.
Stable signing. All debug APKs (CI and local) are signed with the repo-committed
app/debug.keystore (password android, debug-only). A single shared signature is what lets
the in-app updater install a newer build over an older one without a signature mismatch. The
distributed debug build drops the .debug applicationId suffix so updates replace the same
package the user installed.
The ML layer uses frozen, pre-trained TFLite models bundled in
app/src/main/assets/models/ — they run inference on-device but never train or learn
on-device. The weights are fixed at build time; correcting a transaction's category does
not retrain anything. To improve the model you retrain offline and ship a new .tflite.
| File | Shape | Purpose |
|---|---|---|
merchant_classifier.tflite |
float[1][256] → float[1][13] |
SMS merchant → category (13 expense classes, ≥0.40 confidence else dictionary) |
spending_predictor.tflite |
float[1][30][1] → float[1][1] |
End-of-month spend forecast |
behavioral_cluster.tflite |
float[1][7] → float[1][5] |
User archetype (5 clusters) |
Without the model files the app runs entirely on the rule-based classifiers — no runtime
errors. Income categories and any merchant the model can't place fall back to the
dictionary rules + CategoryDefaults.
Dark theme, custom color tokens:
| Token | Hex | Use |
|---|---|---|
Positive |
#4DFFA0 |
Income, success, savings — ONLY positive |
Negative |
#FF6B6B |
Expenses, errors, overrun — ONLY negative |
Warning |
#FFB84D |
70–90% budget usage |
Info |
#4D9FFF |
Links, selection |
Background |
#0A0D12 |
Main background |
All monetary Text uses fontFeatureSettings = "tnum" (tabular-nums).
main ← stable releases only
dev ← integration branch
└── feature/... ← PR to dev → PR to main
SmsReceiveris protected withandroid:permission="android.permission.BROADCAST_SMS"— only the system can broadcast to itSmsReceiver.onReceive()usesgoAsync()so the process is not killed before the DB write completes- All
POST_NOTIFICATIONScalls are guarded by runtime permission check on API 33+ - All Room database inserts in
FosDatabase.PREPOPULATE_CALLBACKuse parameterizedexecSQL()— no SQL injection surface - Coroutine errors in
SmsReceiverare caught byCoroutineExceptionHandler— no silent crashes - CSV export uses RFC 4180 quoting + formula-injection neutralization (
=,+,-,@prefix with') - Deep-link routes are validated against an allowlist before navigation (prevents attacker-controlled crash via exported Activity)
PushNotificationListener→TransactionSource.PUSHrequires user to explicitly enable the notification listener in system Settings- SMS reading is opt-in (
sms_realtime_enabled, defaultfalse) — a fresh install never touches the inbox until the user enables it - Backups are NOT encrypted. The export is plain JSON: the earlier AES-GCM-256 key lived in the Android Keystore and was device-bound, so an encrypted copy could not be restored on a new phone or after a reinstall — it protected the file from its owner.
BackupCrypto.decryptis kept only to read legacy.fosefiles. A backup therefore holds the full transaction history in clear text; treat the file accordingly and don't drop it in a shared folder. Restoring real protection means a user passphrase, never a device key UpdateCheckerrefuses any release asset URL that is nothttps://before opening a connection