|
| 1 | +# Bug Report: NullPointerException in GeofenceForegroundService.onStartCommand |
| 2 | + |
| 3 | +## Package |
| 4 | +- **Name:** geofence_foreground_service |
| 5 | +- **Version:** 1.1.5 |
| 6 | +- **Repository:** https://github.com/Basel-525k/geofence_foreground_service |
| 7 | + |
| 8 | +## Issue Summary |
| 9 | + |
| 10 | +When Android restarts the `GeofenceForegroundService` after it was killed due to memory pressure, the service crashes with a `NullPointerException` because the `intent` parameter in `onStartCommand` is null. |
| 11 | + |
| 12 | +## Environment |
| 13 | + |
| 14 | +- **Device:** Google Pixel (Android 14/15) |
| 15 | +- **Flutter:** 3.x |
| 16 | +- **Package Version:** 1.1.5 |
| 17 | + |
| 18 | +## Steps to Reproduce |
| 19 | + |
| 20 | +1. Start the geofence service with `startGeofencingService()` |
| 21 | +2. Register geofence zones |
| 22 | +3. Put the app in background |
| 23 | +4. Wait for Android to kill the service due to memory pressure (or use `adb shell am kill <package>`) |
| 24 | +5. Observe the service crash when Android tries to restart it |
| 25 | + |
| 26 | +## Expected Behavior |
| 27 | + |
| 28 | +The service should handle null intents gracefully and either: |
| 29 | +- Return `START_STICKY` to retry later |
| 30 | +- Restore state from SharedPreferences and continue |
| 31 | + |
| 32 | +## Actual Behavior |
| 33 | + |
| 34 | +The service crashes with: |
| 35 | + |
| 36 | +``` |
| 37 | +java.lang.RuntimeException: Unable to start service com.f2fk.geofence_foreground_service.GeofenceForegroundService@... with null |
| 38 | +
|
| 39 | +Caused by: java.lang.NullPointerException: Parameter specified as non-null is null: method com.f2fk.geofence_foreground_service.GeofenceForegroundService.onStartCommand, parameter intent |
| 40 | + at com.f2fk.geofence_foreground_service.GeofenceForegroundService.onStartCommand(Unknown Source:2) |
| 41 | +``` |
| 42 | + |
| 43 | +After the crash, Android reschedules the restart with exponential backoff: |
| 44 | +``` |
| 45 | +W ActivityManager: Rescheduling restart of crashed service ... in 2759775ms for mem-pressure-event |
| 46 | +``` |
| 47 | + |
| 48 | +This means the service is offline for **46+ minutes** after each crash! |
| 49 | + |
| 50 | +## Root Cause |
| 51 | + |
| 52 | +In `GeofenceForegroundService.kt`, line 73: |
| 53 | + |
| 54 | +```kotlin |
| 55 | +override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { |
| 56 | +``` |
| 57 | + |
| 58 | +The `intent` parameter is declared as non-nullable (`Intent` instead of `Intent?`), but Android can pass `null` when restarting a service that uses `START_STICKY`. |
| 59 | + |
| 60 | +From Android documentation: |
| 61 | +> If your service returns START_STICKY, the system restarts it with a **null intent** (unless there are pending intents to deliver). |
| 62 | + |
| 63 | +## Proposed Fix |
| 64 | + |
| 65 | +Change the method signature to accept nullable intent and handle it gracefully: |
| 66 | + |
| 67 | +```kotlin |
| 68 | +override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { |
| 69 | + // Handle null intent (service restart after being killed) |
| 70 | + if (intent == null) { |
| 71 | + Log.w(TAG, "Service restarted with null intent - recovering from saved state") |
| 72 | + return recoverFromSavedState(flags, startId) |
| 73 | + } |
| 74 | + |
| 75 | + // Existing code... |
| 76 | + val geofenceAction: GeofenceServiceAction = GeofenceServiceAction.valueOf( |
| 77 | + intent.getStringExtra( |
| 78 | + applicationContext!!.extraNameGen(Constants.geofenceAction) |
| 79 | + )!! |
| 80 | + ) |
| 81 | + // ... |
| 82 | +} |
| 83 | + |
| 84 | +private fun recoverFromSavedState(flags: Int, startId: Int): Int { |
| 85 | + // Option 1: Return START_STICKY to try again later |
| 86 | + // return START_STICKY |
| 87 | + |
| 88 | + // Option 2: Restore from SharedPreferences and restart foreground notification |
| 89 | + val prefs = getSharedPreferences("geofence_service_state", Context.MODE_PRIVATE) |
| 90 | + if (!prefs.getBoolean("is_running", false)) { |
| 91 | + stopSelf() |
| 92 | + return START_NOT_STICKY |
| 93 | + } |
| 94 | + |
| 95 | + val channelId = prefs.getString("channel_id", "default_channel")!! |
| 96 | + val contentTitle = prefs.getString("content_title", "Geofencing active")!! |
| 97 | + val contentText = prefs.getString("content_text", "Monitoring location")!! |
| 98 | + val serviceId = prefs.getInt("service_id", 525600) |
| 99 | + val appIcon = prefs.getInt("app_icon", android.R.drawable.ic_menu_mylocation) |
| 100 | + |
| 101 | + val notification = NotificationCompat.Builder(this, channelId) |
| 102 | + .setOngoing(true) |
| 103 | + .setSmallIcon(appIcon) |
| 104 | + .setContentTitle(contentTitle) |
| 105 | + .setContentText(contentText) |
| 106 | + .build() |
| 107 | + |
| 108 | + startForeground(serviceId, notification, FOREGROUND_SERVICE_TYPE_LOCATION) |
| 109 | + |
| 110 | + // Re-subscribe to location updates |
| 111 | + subscribeToLocationUpdates() |
| 112 | + |
| 113 | + return START_STICKY |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +Additionally, save the service state when starting: |
| 118 | + |
| 119 | +```kotlin |
| 120 | +private fun saveServiceState(channelId: String, contentTitle: String, contentText: String, serviceId: Int, appIcon: Int) { |
| 121 | + getSharedPreferences("geofence_service_state", Context.MODE_PRIVATE) |
| 122 | + .edit() |
| 123 | + .putString("channel_id", channelId) |
| 124 | + .putString("content_title", contentTitle) |
| 125 | + .putString("content_text", contentText) |
| 126 | + .putInt("service_id", serviceId) |
| 127 | + .putInt("app_icon", appIcon) |
| 128 | + .putBoolean("is_running", true) |
| 129 | + .apply() |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +## Workaround for Users |
| 134 | + |
| 135 | +Until this is fixed, users can disable battery optimization for their app: |
| 136 | + |
| 137 | +1. Go to **Settings → Apps → [Your App] → Battery** |
| 138 | +2. Select **Unrestricted** or **Don't optimize** |
| 139 | +
|
| 140 | +This prevents Android from killing the service due to memory pressure. |
| 141 | +
|
| 142 | +## Impact |
| 143 | +
|
| 144 | +- **Severity:** High - Geofencing becomes unreliable |
| 145 | +- **Affected users:** Anyone using the package on Android with memory pressure |
| 146 | +- **User experience:** Geofence events are missed for 46+ minutes after each crash |
| 147 | +
|
| 148 | +## Related |
| 149 | +
|
| 150 | +- Android Service lifecycle documentation: https://developer.android.com/reference/android/app/Service#START_STICKY |
| 151 | +- Similar issues in other packages handling foreground services |
| 152 | +
|
| 153 | +--- |
| 154 | +
|
| 155 | +**Reported by:** VibedTracker App |
| 156 | +**Date:** 2026-01-22 |
0 commit comments