Background location na prawdziwych telefonach: dlaczego w demo działa, a w produkcji pada
W demo śledzenie lokalizacji kierowcy działa bez zarzutu. W produkcji — telefon w kieszeni, aplikacja w tle przez cały dzień — OS zabija aplikację i lokalizacja znika. Jak to zrobić porządnie.
W demo działa perfekcyjnie. Współrzędne GPS przychodzą co 3 sekundy, kierowca na mapie porusza się płynnie, dyspozytor widzi dokładną lokalizację. Demo trwa 20 minut, telefon leży na stole ekranem do góry, aplikacja na pierwszym planie.
W produkcji jest inaczej. Zmiana trwa 8 godzin. Telefon jest w kieszeni albo na uchwycie w samochodzie, ekran wygaszony, aplikacja w tle. Po 45 minutach lokalizacja przestaje przychodzić. Po 2 godzinach dyspozytor widzi kierowcę po raz ostatni na parkingu przed supermarketem.
To nie jest bug w kodzie. To wynik tego, jak nowoczesne mobilne OS zarządzają baterią — i jak OEM producenci dokładają na to własną warstwę.
Dlaczego demo i produkcja to nie to samo
Gdy aplikacja działa na pierwszym planie (foreground), OS daje jej pełny priorytet. Chip GPS wysyła aktualizacje, CPU się nie usypia, wywołania sieciowe przechodzą natychmiast. To jest stan demo.
Gdy użytkownik kliknie przycisk home lub wygasi ekran, aplikacja przechodzi w background. OS zaczyna oszczędzać baterię. Konkretnie oznacza to:
- Android Doze mode (od Android 6): po kilku minutach bez ruchu lub interakcji OS wstrzymuje dostęp do sieci i odkłada alarmy. GPS odbiera, ale pakiety nie docierają na serwer.
- Background App Refresh throttling (iOS): aplikacja w tle dostaje czas CPU tylko od czasu do czasu, na minuty. Między tym nic nie działa.
- Foreground service na Androidzie częściowo rozwiązuje problem — ale tylko częściowo. Utrzymuje proces przy życiu, ale OEM battery manager może go i tak zabić.
Doze mode i foreground service to standard Android. Na to każdy producent dokłada własną warstwę.
Per-OEM rzeczywistość: Samsung, Xiaomi, Huawei, OnePlus
Tu zaczyna się robić nieprzyjemnie. Każdy duży producent Android ma własny system zarządzania baterią, który działa inaczej i wymaga innego permission flow.
Samsung (One UI) ma "Adaptive battery" i "App power management". Aplikacja musi być jawnie wyłączona z "Sleeping apps" — albo ręcznie przez użytkownika, albo przez intent REQUEST_IGNORE_BATTERY_OPTIMIZATIONS. Ale Samsung w nowszych wersjach One UI dodał jeszcze "Background app limits", które działa niezależnie. Musisz obsłużyć jedno i drugie.
Xiaomi (MIUI) jest notoryczne agresywne. MIUI ma "Battery saver" i do tego uprawnienie "Autostart", bez którego aplikacja po restarcie telefonu nigdy nie uruchomi foreground service — nawet gdyby OS nie uśpił. Uprawnienie ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS nie wystarczy. Aplikacja musi sama otworzyć deep link do strony Battery Settings MIUI i poprowadzić użytkownika krok po kroku.
Huawei (EMUI / HarmonyOS) ma listę "Protected apps" i ustawienie "App launch". Bez wpisania na listę Protected apps EMUI zatrzymuje foreground service w ciągu 5–10 minut po wygaszeniu ekranu. Na starszych wersjach EMUI (8, 9) to nie istniało, na nowszych HarmonyOS sytuacja znów jest inna.
OnePlus (OxygenOS) w poprzednich wersjach notoryczne ignorował powiadomienie foreground service i zabijał aplikację. OxygenOS 14+ poprawił sytuację, ale dialog "Battery optimization" nie wystarczy — trzeba nakierować na "Advanced optimization" w ustawieniach baterii.
To jest per-OEM praca. Nie można napisać jednego kodu i zakładać, że działa tak samo na Samsungu i Xiaomi.
Co się dzieje, gdy OS zabije aplikację
Foreground service ma jedną kluczową flagę: START_STICKY. Mówi OS, żeby zrestartował service po zabiciu. Ale restart trwa sekundy albo dziesiątki sekund. W tym czasie przyszły współrzędne GPS, których nikt nie zarejestrował.
Gorsza wersja: OS zabija aplikację i nie restartuje — z powodu memory pressure albo dlatego że MIUI Autostart nie ma uprawnienia. W takim wypadku service w ogóle nie wystartuje, dopóki użytkownik ręcznie nie otworzy aplikacji.
Jak to wykryć? Heartbeat. Aplikacja co N sekund wysyła ping na serwer. Jeśli ping nie przyjdzie przez ponad 2× interwał, serwer wie, że aplikacja jest martwa — nie offline. Ta informacja ma inny biznesowy wpływ niż "kierowca nie ma sygnału".
Po stronie aplikacji: przy starcie sprawdzamy timestamp ostatniej udanej współrzędnej GPS. Jeśli jest starsza niż 5 minut i aplikacja właśnie wystartowała, wiemy, że był restart po zabiciu. Loguj to, raportuj do systemu monitoringu.
Offline queue i recovery
Współrzędnych GPS, które przychodzą podczas braku łączności, nie można odrzucić. Dyspozytor musi wiedzieć, którędy jechał kierowca — nawet jeśli w tunelu nie miał sygnału.
Rozwiązanie: lokalna kolejka współrzędnych. Każda współrzędna zapisuje się do lokalnego storage (SQLite, Hive, SharedPreferences — zależy od platformy). Osobny worker thread periodycznie próbuje wysłać współrzędne na serwer. Przy sukcesie usuwa je. Przy błędzie zostawia i próbuje ponownie z exponential backoff.
Kolejka musi mieć górny limit — nie można akumulować współrzędnych w nieskończoność. Rozsądny limit to 2–4 godziny zapisu przy normalnej częstotliwości. Starsze rekordy są odrzucane, ale zapisuje się fakt, że dane z tego okresu są niedostępne.
Recovery po restarcie: przy uruchomieniu aplikacji worker sprawdza, czy kolejka zawiera niewysłane rekordy, i wysyła je w partiach. Serwer musi obsługiwać odbiór współrzędnych out-of-order i idempotentny zapis (ta sama współrzędna wysłana dwukrotnie nie może tworzyć duplikatu).
Semantyka online/offline i heartbeat
"Kierowca jest offline" to niejednoznaczny stan. Może oznaczać:
- Kierowca celowo wyłączył dostępność (przerwa, koniec zmiany)
- Aplikacja straciła sygnał GPS (tunel, garaż)
- Aplikacja straciła połączenie sieciowe
- OS zabił aplikację w tle
- Telefon się rozładował
Dla systemu dispatch to różne sytuacje z różnymi akcjami. System heartbeat je rozróżnia:
- Pauza GPS bez utraty heartbeat = sygnał brakuje, ale aplikacja działa
- Brak heartbeatu, ale ostatnia wiadomość to "going offline" = celowe rozłączenie
- Brak heartbeatu bez wiadomości = aplikacja martwa lub bateria wyczerpana
Interwał heartbeatu musi być kompromisem. Co 30 sekund to za często — pożera baterię i przeciąża serwer przy 100 podłączonych kierowcach. Co 5 minut to za rzadko — przez 5 minut nie wiesz, czy aplikacja żyje. Rozsądny kompromis to 60–90 sekund, przy czym serwer oznacza kierowcę jako "suspect offline" po 3 nieodebranych heartbeatach (2,5–4,5 minuty latencji).
Permission flow: zaprojektować z góry, nie dołatać
Permission flow dla background location musi być zaprojektowany jako część onboardingu, nie jako afterthought. Użytkownik musi rozumieć, dlaczego aplikacja potrzebuje lokalizacji "always" (not just while using the app), inaczej odrzuci to w systemowym dialogu.
Na Androidzie oznacza to stopniowy request: najpierw "While using", potem wyjaśnienie dlaczego "Always", potem request o "Always" przez ustawienia (systemowy dialog nie pozwala na bezpośrednie przyznanie "Always" dla background — użytkownik musi wejść do Settings). Ten flow Google zaprojektował celowo jako friction. Nie można go obejść, można go tylko dobrze wyjaśnić.
Na iOS: "When In Use" → CoreLocation usage description → request upgrade na "Always" → opis dlaczego. iOS 14+ dodał przełącznik "Precise location" / "Approximate location" — śledzenie lokalizacji kierowcy wymaga Precise, i użytkownik musi to świadomie zatwierdzić.
Po przyznaniu "Always" nie zapominać sprawdzać ponownie przy każdym uruchomieniu. Użytkownik może odebrać uprawnienie w dowolnym momencie w Settings — bez powiadomienia aplikacji.
Telemetria: bez danych nie masz problemu, masz tylko skargi
Aplikacja bez telemetrii daje ci tylko komunikaty od kierowców: "lokalizacja nie działała". Nie wiesz kiedy, gdzie, jaki telefon, jaki Android, czy to był Doze, MIUI kill czy awaria sieci.
Minimalna telemetria dla background location:
- Liczba aktualizacji GPS na godzinę (anomalia = Doze lub kill)
- Liczba heartbeat failures i ich długość
- Eventy restartu z flagą "po kill" vs "celowy start"
- Wersja OS i model telefonu przy każdym evencie
Z tymi danymi widzisz: Xiaomi MIUI 14 odpowiada za 60 % kill eventów, mimo że stanowi 20 % floty. Samsung One UI 6 ma problem po 23:00, gdy agresywniej zasypia. To są realne liczby, które widzieliśmy w Carivio i TaxiLight.
Bez telemetrii problem istnieje, ale nie masz dźwigni, żeby go priorytetyzować ani udowodnić, że fix zadziałał.
Uczciwe podsumowanie
Background location w aplikacji dispatch to nie problem one-size-fits-all. To suma: OS battery management + per-OEM warstwa + permission flow + offline queue + heartbeat + kill detection + telemetria.
Demo działa, bo eliminuje większość z tego. Aplikacja jest na pierwszym planie, telefon na stole, sesja trwa 20 minut. Produkcja tego wyeliminować nie może.
Tego nie da się rozwiązać jednym ustawieniem. To praca inżynierska, którą trzeba zaprojektować z góry — nie dołatywać po skargach kierowców.
Jeśli budujesz system dispatch lub aplikację kierowcy i nie chcesz odkrywać problemów dopiero w produkcji, napisz do nas — przejdziemy przez architekturę i powiemy, gdzie są dziury.
FAQ
Czy background location na iOS jest bardziej niezawodne niż na Androidzie?
Na iOS sytuacja jest bardziej przewidywalna — Apple daje CLLocationManager względnie spójne zachowanie i nie istnieje ekosystem OEM battery optimizations jak na Androidzie. Ale iOS też nie jest odporny: po przejściu w tryb background aplikacja dostaje ograniczony czas, uprawnienie "always" wymaga więcej kroków akceptacji od użytkownika, a Low Power Mode throttluje lokalizację. Na Androidzie problemów jest więcej, ale są przynajmniej udokumentowane i rozwiązywalne per-OEM.
Czy wystarczy skonfigurować foreground service i po sprawie?
Foreground service to warunek konieczny, nie wystarczający. Zapobiega zabijaniu procesu przez Androida przy normalnym ciśnieniu pamięci. Ale nie ochroni cię przed użytkownikiem, który ręcznie kliknie "Force stop", przed agresywnym menedżerem baterii MIUI, ani przed trybem Doze, jeśli nie masz poprawnie skonfigurowanego wake lock. Foreground service to podstawa — na wierzchu potrzebujesz kill detection, offline queue i heartbeat.
Czy muszę implementować per-OEM obsługę dla każdego producenta z osobna?
Tak, jeśli chcesz niezawodności w produkcji. Nie można napisać jednego kodu i zakładać, że działa tak samo wszędzie. W praktyce oznacza to: wykrywanie producenta w runtime, kierowanie użytkownika do właściwych ustawień systemowych przez deep link (na dontkillmyapp.com są udokumentowane URL schematy dla MIUI, EMUI, OxygenOS), i testowanie na fizycznych urządzeniach każdego producenta — nie tylko na emulatorze. Emulatory nie symulują battery optimizations.