«Нельзя управлять тем, что нельзя измерить.»— Питер Друкер
Эта статья посвящена созданию Android-приложения для решения практической задачи — отслеживания местоположения коммерческих представителей в полевых условиях. Статья может быть полезна широкому кругу Android-разработчиков как с точки зрения использования готового решения, так и с точки зрения реализации отдельных компонентов приложения для применения в своих проектах.
Работа коммерческого представителя обычно связана с ежедневным посещением клиентов по заранее спланированному маршруту. Для организации, в которой работают такие сотрудники, важно контролировать маршруты, анализировать посещения клиентов и вести отчётность. Одним из решений является разработка специализированного приложения, которое работает на мобильных устройствах представителей и осуществляет фоновый трекинг перемещений с последующей передачей данных на сервер. В текущей реализации серверная часть и API отсутствуют (используется заглушка для последующей доработки).
Архитектура и основные компоненты приложения
Приложение Open Tracker спроектировано для полностью автоматической работы после первоначальной установки и настройки, что минимизирует необходимость взаимодействия с пользователем. Центральным компонентом архитектуры является Service, обеспечивающий долговременную работу в фоне.
На структурной схеме представлены основные компоненты системы и их взаимодействие:

Ключевые компоненты:
Service - реализует основную логику приложения и обеспечивает фоновую работу
TimeManager - определяет рабочее время и дни, активируя трекинг только в рабочие периоды для экономии заряда батареи (с возможностью включения постоянного трекинга для тестирования)
LogManager - объединяет логику сбора, хранения и отправки геоданных
LocationManager, NetSender, FileSaver - отвечают соответственно за сбор координат, их отправку и локальное хранение
BroadcastReceiver - обеспечивает автоматический запуск после перезагрузки устройства и обработку изменений системного времени
Activity - реализует пользовательский интерфейс на основе Compose с архитектурой MVVM и навигацией через Compose Navigation
Реализация ключевых функций
Организация фоновой работы
Для долговременной работы в фоне используется ForegroundService, который отображает уведомление, позволяющее пользователю контролировать состояние трекинга. После окончания рабочего времени Service прекращает показ уведомления и может быть уничтожен системой.
Особенностью реализации является комбинированный подход: Service работает как в режиме Started (автономно), так и Bound (с возможностью связи с UI).
private fun startForeground() {
val notification = NotificationUtils.getForegroundNotification(this)
val type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
} else {
0
}
ServiceCompat.startForeground(
this,
NotificationConstants.FOREGROUND_NOTIFICATION_ID,
notification,
type
)
}
private fun stopForeground() {
stopForeground(STOP_FOREGROUND_REMOVE)
}
Периодический запуск трекинга
Для обеспечения периодического выполнения трекинга используется AlarmManager внутри Service. Функция restartTimer
принимает два параметра:
Время следующего запуска (в миллисекундах)
Интервал повторения (в миллисекундах)
Для стабильной работы в фоне используется режим ELAPSED_REALTIME_WAKEUP
, который гарантирует пробуждение устройства через заданные интервалы времени. Важное требование — отключение режима оптимизации энергосбережения для приложения.
private fun restartTimer(
futureTriggerTime: Long,
interval: Long = TRACKER_LOCATION_POINT_INTERVAL
) {
val intent = Intent(this, TrackerReceiver::class.java).apply {
action = TRACKER_TIMER_ACTION
}
val tTime = SystemClock.elapsedRealtime() + futureTriggerTime - System.currentTimeMillis()
val pIntent = PendingIntent.getBroadcast(
this, TRACK_TIMER_CODE, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
(getSystemService(ALARM_SERVICE) as AlarmManager).setRepeating(
AlarmManager.ELAPSED_REALTIME_WAKEUP, tTime, interval, pIntent
)
}
Дополнительно применяется WakeLock для гарантированного выполнения всех необходимых операций после пробуждения устройства.
private val lock: WakeLock by lazy {
(getSystemService(POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_NAME).apply {
setReferenceCounted(true)
}
}
}
Асинхронная обработка
Все ресурсоемкие операции в Service выполняются с использованием Kotlin Coroutines. Для перехода между обычным и suspend-кодом используется специально настроенный SharedFlow (commands), а для запуска корутин — CoroutineScope (serviceScope).
class TrackerService : Service() {
private val serviceScope = CoroutineScope(Dispatchers.Main.immediate)
private val _trackerHistory = MutableStateFlow(emptyList<PositionData>())
private val _trackerState = MutableStateFlow(TrackerState())
private val commands = MutableSharedFlow<String>(
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_LATEST
)
Конфигурация SharedFlow обеспечивает контроль над частотой выполнения фоновых задач, предотвращая перегрузку системы. Команды (Action) поступают из Intent, полученного в методе onStartCommand
и далее обрабатываются в handleAction.
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
acquireWakeLock(WAKE_LOCK_TIMEOUT)
handleAction(intent?.action ?: "")
return START_NOT_STICKY
}
private fun handleAction(action: String) = when (action) {
Intent.ACTION_BOOT_COMPLETED,
Intent.ACTION_MY_PACKAGE_REPLACED,
Intent.ACTION_DATE_CHANGED,
Intent.ACTION_TIME_CHANGED -> handleSystemAction(action)
TRACKER_CLIENT_BIND -> handleClientBind()
TRACKER_TIMER_ACTION -> handleTimerAction()
else -> Unit
}
В методе handleTimerAction
происходит отправка команды через commands на запуск фоновой задачи
commands.tryEmit("start")
и далее выполнение в методе collect.
serviceScope.launch(handler) {
commands.collect { startLogging(logManager) }
}
Сбор и обработка геоданных
Логика работы с геоданными вынесена в отдельный класс LocationManager что позволяет модифицировать её без изменения базовой логики Service. Для сбора координат используется FusedLocationProvider как наиболее энергоэффективное решение.
Основной метод getPoints
является suspend-функцией и поддерживает отмену операций, возвращая список собранных геоданных.
override suspend fun getPoints() = suspendCancellableCoroutine { continuation ->
start { positions ->
if (continuation.isActive) {
continuation.resume(positions)
}
}
continuation.invokeOnCancellation {
stop()
}
}
Процесс сбора данных начинается с вызова метода start
, который:
Запускает подписку на обновления местоположения
Ограничивает время сбора константой COLLECT_TIMEOUT (1 минута)
Возвращает накопленные данные по истечении времени или при достижении лимита точек (POSITIONS_LIMIT)
private fun start(callBack: (List<PositionData>) -> Unit) {
resultCallback = callBack
positionList.clear()
val permResult = runCatching {
val builder = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000).apply {
setMinUpdateDistanceMeters(0f)
setWaitForAccurateLocation(true)
}
fusedManager.requestLocationUpdates(builder.build(), locListener, Looper.getMainLooper())
}
if (permResult.isSuccess) {
handler.postDelayed(handleStop, COLLECT_TIMEOUT)
} else {
stop()
sendResult()
}
}
Для обеспечения качества данных реализована фильтрация:
Отсеивание подложных координат
Игнорирование устаревших данных
Возможность использования последних известных координат при отсутствии новых
Резервный вариант сбора данных GSM вышек (не реализован, но предусмотрена архитектура)
private fun handleLocationPoint(it: Location) {
if (isLocationTooOld(it) || isFake(it)) return
positionList.add(PositionGpsData(gpsLocation = it))
if (positionList.size > POSITIONS_LIMIT) stopWork()
}
Сохранение геоданных: надежность и аналитика
Реализация логики сохранения геоданных в классе FileSaver обеспечивает гибкость и возможность адаптации под специфические требования проекта.
Ключевые особенности:
-
Формат данных
-
Основной метод
save
являетсяsuspend
-функцией, принимающей:log
— список координат и системных событий (например, изменение системного времени, ошибки GPS и тд.).dirPath
— путь для сохранения файлов.makeNow
— признак формирования пакета для отправки.
-
Сохранение не только координат, но и системных событий позволяет:
Анализировать работу приложения в фоне.
Выявлять потенциальные проблемы (например, отключение GPS пользователем).
-
-
Механизм записи
-
Данные сохраняются в файлы с контролируемым жизненным циклом:
Сначала создается временный файл с расширением
.wrk
.После интервала
TIME_INTERVAL_TO_CHANGE_FILE
файл переименовывается в.txt
— это гарантирует целостность данных при передаче на сервер.
Возвращаемое значение — список готовых к отправке файлов (пакетов).
-
override suspend fun save(dirPath: String, log: List<String>, makeNow: Boolean): Result<List<String>> {
return runCatching {
if (currentLogFile == null) {
currentLogFile = makeNewLogFile(dirPath)
}
saveToFile(log)
if (makeNow || System.currentTimeMillis() > nextRenameTime || abs(System.currentTimeMillis() - nextRenameTime) > TIME_INTERVAL_TO_CHANGE_FILE) {
renameLogFile(dirPath)
nextRenameTime = System.currentTimeMillis() + TIME_INTERVAL_TO_CHANGE_FILE
}
currentLogFile = null
// Возвращаем список файлов с расширением .txt в директории
File(dirPath).listFiles()?.filter { it.name.endsWith(DATA_FILE_EXT_TXT) }?.map { it.absolutePath } ?: emptyList()
}
}
Отправка пакетов на сервер: архитектура и заглушка
Логика передачи данных на сервер реализована в классе NetSender. В текущей версии проекта API сервера не определён, поэтому класс имитирует отправку пакетов — после обработки файлы удаляются, что позволяет тестировать основной функционал приложения без реального бэкенда.
Ключевые особенности реализации
-
Гибкость интеграции
Класс NetSender спроектирован так, чтобы в будущем его можно было адаптировать под любой протокол (REST, WebSocket, gRPC) или формат данных (JSON, Protobuf).
В текущей реализации метод
send
принимает список файлов и "успешно отправляет" их, после чего удаляет с устройства.
-
Заглушка вместо реального API
-
Такой подход позволяет:
Проверять работу цепочки LoсationManager → FileSaver → NetSender.
Тестировать обработку ошибок (например, отсутствие интернета) в будущем.
Заключение по первой части
В этой части статьи рассмотрено ядро приложения Service — от сбора геоданных до их сохранения и отправки на сервер. Ключевые особенности решения:
Энергоэффективность: Фоновый Service, AlarmManager и оптимизация GPS-запросов.
Гибкость: Модульная архитектура (LoсationManager, FileSaver, NetSender) позволяет адаптировать логику под разные бизнес-задачи.
Масштабируемость: Заглушки вместо реального API упрощают тестирование и будущую интеграцию с бэкендом.
-
В части 2 статьи будет рассмотрена реализация пользовательского интерфейса приложения.
GuryanovIlya
Как обрабатываете попытки скама с стороны пользователей?
Например: отобрать разрешение на работу с координатами в фоне, попытки руками остановить сервис и самое жесткое и не приятное это режим ультра экономии энергии?
colalike Автор
На личных мобильных устройствах пользователь всегда может сознательно или случайно нарушить работу трекера: отключить разрешения, активировать режим энергосбережения или вовсе удалить приложение. Поскольку это персональное устройство, администрация не может полностью исключить такие ситуации - приложение работает в рамках предоставленных пользователем прав.
Для корпоративных устройств, управляемых через MDM-системы (Mobile Device Management), эти риски минимизированы. Администратор может:
заблокировать изменение критических настроек
запретить удаление приложения
дистанционно восстановить необходимые разрешения
С точки зрения трудовой дисциплины, коммерческий представитель должен предоставлять ежедневный отчет о маршруте. Намеренное препятствование работе трекера противоречит его профессиональным обязанностям и может рассматриваться как нарушение.
Трекер должен фиксировать и передавать на сервер дополнительную информацию: текущий статус разрешений, изменениях системных настроек и тд. Эти данные позволяют отличать сбои от намеренных действий, оказывать адресную помощь сотруднику.