Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions docs/background_tracking_history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Background Tracking Reference

## Historical Timeline

| Date (UTC) | Commit | Description |
| --- | --- | --- |
| 2020-07-13 | 6385b0a81b7ae6efde11a6a56b5fa275cba5b310 | First introduction of `flutter_background_geolocation`; Android-only `_bgTracking` listener scheduled continuous fixes with plugin-managed persistence. |
| 2022-04-01 | 266a19d605d7ed5aa579dcdc2aaf84abc70c2285 | Settings drawer still invoked `_bgTracking()` in `lib/pages/main/main_vc.dart`, confirming the plugin remained authoritative for background capture/masking (0.025° grid) and automatic retry. |
| 2024-04-11 | 7831ce40e3e78c755a8f09f968a840bf9bffa136 | Plugin removed; Workmanager + Geolocator periodic task added in `lib/main.dart`. Fix uploads became fire-and-forget HTTP calls, so offline devices silently dropped samples. |
| 2025-11-27 | (feature/offline-report-queue, uncommitted) | Completed migration to first-party tracking queue in `lib/utils/BackgroundTracking.dart` with SharedPreferences persistence, masking, and connectivity-aware retries. |

## Sampling Strategy Timeline

| Date Range | Mechanism | Notes |
| --- | --- | --- |
| 2020-07 → 2024-04 | `flutter_background_geolocation` with `locationUpdateInterval = 4h48m` | Deterministic cadence (~5 fixes/day) evenly spaced over 24h; plugin buffered offline data automatically. |
| 2024-04 → 2025-03 | `Workmanager.registerPeriodicTask('backgroundTracking', ...)` | Still deterministic 4h48m cadence, but fixes were only sent immediately; failures were dropped because no queue existed. |
| 2025-03 → 2025-11 | `BackgroundTracking.scheduleDailyTrackingTask` (fraction-of-day scaling) | Each midnight run sampled uniformly within the remaining portion of the day; enabling tracking late meant fewer than five runs could execute that day, mirroring legacy behavior users already saw. |
| 2025-11-XX → 2025-11-27 | Temporary "schedule all remaining slots" experiment | Always queued every unsent slot, removing the scaling heuristic but deviating from production expectations. |
| 2025-11-27 onward | Fractional scaling restored + offline persistence | Reinstated the scaling heuristic while keeping the new persistence, masking, and connectivity-aware retries. |

### How Fractional Scaling Works

- Let `tasksPerDay = 5` and `numScheduledTasks` be how many runs have already executed today.
- When tracking turns on, we compute the remaining fraction of the current day: `remaining = 1 - (elapsedSeconds / 86,400)`.
- Number of random slots to queue immediately = `max(1, ceil((tasksPerDay - numScheduledTasks) * remaining))`.
- Example: enabling at 14:00 (50,400 seconds elapsed) yields `remaining ≈ 0.4167`; with zero runs so far we schedule `ceil(5 * 0.4167) = 3` random fixes between 14:00 and 23:59.
- At midnight a new pass runs, resetting counts and sampling five fresh times across the full next day.

## Requirements To Restore Legacy Reliability

1. Capture exactly five fixes per day at random times whenever background tracking is enabled, regardless of connectivity.
2. Mask each fix immediately using the historical 0.025° grid.
3. Persist every masked fix locally until the API acknowledges ingestion.
4. Retry syncing automatically whenever the device regains connectivity; no dependency on the third-party plugin.

## Implementation Plan & Status (Nov 27 2025)

- [x] Centralize scheduling, masking, queueing, and sync logic in `lib/utils/BackgroundTracking.dart`.
- [x] Persist pending fixes as JSON in SharedPreferences and attach a deterministic `trackingUUID` so the server can group coverage windows.
- [x] Guarantee five random executions per calendar day (removed the fractional-day throttle so we always schedule the remaining slots).
- [x] Automatically register a Workmanager job that retries `syncPendingFixes()` once the OS reports the network is available.
- [x] Document verification steps (manual + automated) so we can regress-test after implementation.

## Implementation Notes – Nov 27 2025

- `lib/utils/BackgroundTracking.dart`
- `scheduleDailyTrackingTask` once again scales the number of random samples by the remaining fraction of the day (matching legacy cadence expectations) while still giving each Workmanager task a unique ID.
- Masked fixes are persisted immediately and, if syncing fails, we register a `syncPendingFixes` Workmanager one-off job that requires connectivity. Once the queue drains we cancel that job.
- `lib/main.dart`
- `callbackDispatcher` now understands the new `syncPendingFixes` task and passes a `TimeOfDay` floor into each nightly scheduling run.
- Validation checklist:
1. Toggle tracking on and confirm five `trackingTask` registrations appear for the current day.
2. Trigger a tracking run while offline, verify the fix enters `_pendingFixes`, then come back online and confirm the `syncPendingFixes` job flushes it immediately.
3. Run `fvm flutter analyze` + `fvm flutter test` before committing.

## Next Steps

1. Capture console logs/screenshots of the manual validation scenarios above for documentation.
2. Consider lightweight integration tests that stub Workmanager to assert scheduling counts.
3. Explore telemetry (e.g., Sentry breadcrumbs) for queued vs. synced fix counts to monitor production health.

(Keep this document updated as we implement and test the remaining steps.)
24 changes: 18 additions & 6 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -343,18 +343,24 @@
"${BUILT_PRODUCTS_DIR}/FirebaseMessaging/FirebaseMessaging.framework",
"${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework",
"${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework",
"${BUILT_PRODUCTS_DIR}/Mantle/Mantle.framework",
"${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImageWebPCoder/SDWebImageWebPCoder.framework",
"${BUILT_PRODUCTS_DIR}/app_set_id/app_set_id.framework",
"${BUILT_PRODUCTS_DIR}/battery_plus/battery_plus.framework",
"${BUILT_PRODUCTS_DIR}/camera_avfoundation/camera_avfoundation.framework",
"${BUILT_PRODUCTS_DIR}/country_codes/country_codes.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/flutter_image_compress_common/flutter_image_compress_common.framework",
"${BUILT_PRODUCTS_DIR}/flutter_native_splash/flutter_native_splash.framework",
"${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework",
"${BUILT_PRODUCTS_DIR}/geocoding_ios/geocoding_ios.framework",
"${BUILT_PRODUCTS_DIR}/geolocator_apple/geolocator_apple.framework",
"${BUILT_PRODUCTS_DIR}/image_picker_ios/image_picker_ios.framework",
"${BUILT_PRODUCTS_DIR}/in_app_review/in_app_review.framework",
"${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework",
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
"${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
"${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework",
Expand All @@ -373,18 +379,24 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseMessaging.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mantle.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImageWebPCoder.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/app_set_id.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/battery_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera_avfoundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/country_codes.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_image_compress_common.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_native_splash.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/geocoding_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/geolocator_apple.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_review.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework",
Expand Down Expand Up @@ -553,7 +565,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 45253FTRX4;
DEVELOPMENT_TEAM = 6MGZ4KYJ2V;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
Expand All @@ -572,7 +584,7 @@
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
NEW_SETTING = "";
NEW_SETTING1 = "";
PRODUCT_BUNDLE_IDENTIFIER = cat.ibeji.tigatrapp2;
PRODUCT_BUNDLE_IDENTIFIER = cat.ibeji.tigatrapp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
Expand Down Expand Up @@ -702,7 +714,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 45253FTRX4;
DEVELOPMENT_TEAM = 6MGZ4KYJ2V;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
Expand All @@ -720,7 +732,7 @@
);
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
NEW_SETTING1 = "";
PRODUCT_BUNDLE_IDENTIFIER = cat.ibeji.tigatrapp2;
PRODUCT_BUNDLE_IDENTIFIER = cat.ibeji.tigatrapp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
Expand All @@ -743,7 +755,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 45253FTRX4;
DEVELOPMENT_TEAM = 6MGZ4KYJ2V;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
Expand All @@ -762,7 +774,7 @@
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
NEW_SETTING = "";
NEW_SETTING1 = "";
PRODUCT_BUNDLE_IDENTIFIER = cat.ibeji.tigatrapp2;
PRODUCT_BUNDLE_IDENTIFIER = cat.ibeji.tigatrapp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
Expand Down
Loading