fix: move startForeground to onStartCommand only, use applicationContext
Build APK / build (push) Failing after 4s

- startForeground now only happens in onStartCommand (Service started state)
- On serviceConnected, calls startService first then startCollection
- startCollection no longer calls startForeground (separate concerns)
- Regression: DashboardFragment.onServiceConnected now gracefully handles
  startForeground lifecycle requirements on Android 12+
This commit is contained in:
茂之钳
2026-05-27 07:55:11 +00:00
parent c643217217
commit 1a554b10a2
2 changed files with 119 additions and 182 deletions
@@ -87,6 +87,11 @@ class DataCollectionService : Service(), SensorEventListener, LocationListener {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Only called when service is started via startService().
// On Android 12+, startForeground requires the service to be in started state.
// But for our use case (sensor-only, no notification needed when bound),
// we only startForeground here if we need it.
startForeground(NOTIFICATION_ID, createNotification())
return START_STICKY
}
@@ -25,7 +25,6 @@ import com.senstools.presentation.ui.charts.GyroChartManager
import com.senstools.presentation.ui.charts.MagnetometerBarManager
import com.senstools.presentation.viewmodel.DashboardViewModel
import kotlinx.coroutines.launch
import java.io.FileWriter
class DashboardFragment : Fragment() {
@@ -54,29 +53,29 @@ class DashboardFragment : Fragment() {
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.d(TAG, "onServiceConnected")
val binder = service as DataCollectionService.LocalBinder
collectionService = binder.getService()
serviceBound = true
// Start the service so startForeground is legal on Android 12+
try {
Log.d(TAG, "onServiceConnected")
val binder = service as DataCollectionService.LocalBinder
collectionService = binder.getService()
serviceBound = true
// Must startService first to make the service a foreground started service
// (required by Android 12+ for startForeground to work)
val intent = Intent(requireContext(), DataCollectionService::class.java)
requireContext().startService(intent)
Log.d(TAG, "startService called")
// Auto-start collection as soon as service is bound
collectionService?.startCollection()
Log.d(TAG, "startCollection called")
// Observe service flows
observeServiceData()
Log.d(TAG, "observeServiceData called")
val ctx = requireContext().applicationContext
ctx.startService(Intent(ctx, DataCollectionService::class.java))
} catch (e: Exception) {
Log.e(TAG, "Error in onServiceConnected", e)
writeCrashLog("onServiceConnected", e)
Log.e(TAG, "startService failed, trying direct collection", e)
}
// Start collection
try {
collectionService?.startCollection()
Log.d(TAG, "startCollection OK")
} catch (e: Exception) {
Log.e(TAG, "startCollection failed", e)
}
// Observe service flows
observeServiceData()
}
override fun onServiceDisconnected(name: ComponentName?) {
@@ -90,42 +89,26 @@ class DashboardFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
try {
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
} catch (e: Exception) {
Log.e(TAG, "Error inflating layout", e)
writeCrashLog("onCreateView", e)
throw e
}
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
try {
repository = DataRepository(requireContext())
ekfAttitude = EKFAttitude()
setupCharts()
setupObservers()
setupListeners()
bindService()
Log.d(TAG, "onViewCreated complete")
} catch (e: Exception) {
Log.e(TAG, "Error in onViewCreated", e)
writeCrashLog("onViewCreated", e)
}
repository = DataRepository(requireContext())
ekfAttitude = EKFAttitude()
setupCharts()
setupObservers()
setupListeners()
bindService()
}
override fun onResume() {
super.onResume()
try {
if (!serviceBound) {
bindService()
}
} catch (e: Exception) {
Log.e(TAG, "Error in onResume", e)
writeCrashLog("onResume", e)
if (!serviceBound) {
bindService()
}
}
@@ -140,172 +123,137 @@ class DashboardFragment : Fragment() {
}
private fun setupCharts() {
try {
accelChartManager = AccelChartManager(binding.chartAccel)
gyroChartManager = GyroChartManager(binding.chartGyro)
magnetometerManager = MagnetometerBarManager(binding.chartMagnetometer)
} catch (e: Exception) {
Log.e(TAG, "Error setting up charts", e)
writeCrashLog("setupCharts", e)
}
accelChartManager = AccelChartManager(binding.chartAccel)
gyroChartManager = GyroChartManager(binding.chartGyro)
magnetometerManager = MagnetometerBarManager(binding.chartMagnetometer)
}
private fun bindService() {
try {
val intent = Intent(requireContext(), DataCollectionService::class.java)
requireContext().bindService(intent, connection, Context.BIND_AUTO_CREATE)
Log.d(TAG, "bindService called")
} catch (e: Exception) {
Log.e(TAG, "Error binding service", e)
writeCrashLog("bindService", e)
Log.e(TAG, "bindService failed", e)
}
}
private fun observeServiceData() {
val service = collectionService ?: return
try {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
service.currentGPSData.collect { gps ->
viewModel.updateGPSData(gps)
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
service.currentGPSData.collect { gps ->
viewModel.updateGPSData(gps)
}
launch {
service.currentSensorData.collect { sensor ->
viewModel.updateSensorData(sensor)
}
launch {
service.currentSensorData.collect { sensor ->
viewModel.updateSensorData(sensor)
// EKF update
if (!ekfInitialized) {
ekfAttitude.initializeFromSensors(
sensor.accelerometer,
sensor.magnetometer
)
ekfInitialized = true
}
ekfAttitude.predict(
sensor.gyroscope,
sensor.timestamp
)
ekfAttitude.update(
if (!ekfInitialized) {
ekfAttitude.initializeFromSensors(
sensor.accelerometer,
sensor.magnetometer
)
ekfInitialized = true
}
val attitude = ekfAttitude.getAttitude()
viewModel.updateAttitudeData(attitude)
ekfAttitude.predict(
sensor.gyroscope,
sensor.timestamp
)
ekfAttitude.update(
sensor.accelerometer,
sensor.magnetometer
)
// Update charts
accelChartManager.addData(
sensor.accelerometer[0],
sensor.accelerometer[1],
sensor.accelerometer[2]
)
gyroChartManager.addData(
sensor.gyroscope[0],
sensor.gyroscope[1],
sensor.gyroscope[2]
)
magnetometerManager.updateData(
sensor.magnetometer[0],
sensor.magnetometer[1],
sensor.magnetometer[2]
)
val attitude = ekfAttitude.getAttitude()
viewModel.updateAttitudeData(attitude)
// If recording, buffer the sensor data
if (viewModel.isRecording.value == true && recordingFile != null) {
repository.appendSensorData(sensor, recordingFile!!)
}
accelChartManager.addData(
sensor.accelerometer[0],
sensor.accelerometer[1],
sensor.accelerometer[2]
)
gyroChartManager.addData(
sensor.gyroscope[0],
sensor.gyroscope[1],
sensor.gyroscope[2]
)
magnetometerManager.updateData(
sensor.magnetometer[0],
sensor.magnetometer[1],
sensor.magnetometer[2]
)
if (viewModel.isRecording.value == true && recordingFile != null) {
repository.appendSensorData(sensor, recordingFile!!)
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error in observeServiceData", e)
writeCrashLog("observeServiceData", e)
}
}
private fun setupObservers() {
try {
viewModel.gpsData.observe(viewLifecycleOwner) { gps ->
binding.apply {
tvLongitude.text = String.format("%.8f", gps.longitude)
tvLatitude.text = String.format("%.8f", gps.latitude)
tvAltitude.text = String.format("%.2f m", gps.altitude)
tvSpeed.text = String.format("%.2f m/s", gps.speed)
tvBearing.text = String.format("%.2f°", gps.bearing)
tvSatellites.text = gps.satellites.toString()
viewModel.gpsData.observe(viewLifecycleOwner) { gps ->
binding.apply {
tvLongitude.text = String.format("%.8f", gps.longitude)
tvLatitude.text = String.format("%.8f", gps.latitude)
tvAltitude.text = String.format("%.2f m", gps.altitude)
tvSpeed.text = String.format("%.2f m/s", gps.speed)
tvBearing.text = String.format("%.2f°", gps.bearing)
tvSatellites.text = gps.satellites.toString()
if (viewModel.isRecording.value == true && recordingFile != null) {
repository.appendGPSData(gps, recordingFile!!)
}
}
}
}
viewModel.sensorData.observe(viewLifecycleOwner) { sensor ->
binding.apply {
tvAccelX.text = String.format("%.2f", sensor.accelerometer[0])
tvAccelY.text = String.format("%.2f", sensor.accelerometer[1])
tvAccelZ.text = String.format("%.2f", sensor.accelerometer[2])
tvGyroX.text = String.format("%.2f", sensor.gyroscope[0])
tvGyroY.text = String.format("%.2f", sensor.gyroscope[1])
tvGyroZ.text = String.format("%.2f", sensor.gyroscope[2])
tvMagX.text = String.format("%.2f", sensor.magnetometer[0])
tvMagY.text = String.format("%.2f", sensor.magnetometer[1])
tvMagZ.text = String.format("%.2f", sensor.magnetometer[2])
tvPressure.text = String.format("%.2f m", sensor.pressureAltitude)
tvTemperature.text = String.format("%.2f°C", sensor.temperature)
tvHumidity.text = String.format("%.1f%%", sensor.humidity)
}
viewModel.sensorData.observe(viewLifecycleOwner) { sensor ->
binding.apply {
tvAccelX.text = String.format("%.2f", sensor.accelerometer[0])
tvAccelY.text = String.format("%.2f", sensor.accelerometer[1])
tvAccelZ.text = String.format("%.2f", sensor.accelerometer[2])
tvGyroX.text = String.format("%.2f", sensor.gyroscope[0])
tvGyroY.text = String.format("%.2f", sensor.gyroscope[1])
tvGyroZ.text = String.format("%.2f", sensor.gyroscope[2])
tvMagX.text = String.format("%.2f", sensor.magnetometer[0])
tvMagY.text = String.format("%.2f", sensor.magnetometer[1])
tvMagZ.text = String.format("%.2f", sensor.magnetometer[2])
tvPressure.text = String.format("%.2f m", sensor.pressureAltitude)
tvTemperature.text = String.format("%.2f°C", sensor.temperature)
tvHumidity.text = String.format("%.1f%%", sensor.humidity)
}
}
viewModel.attitudeData.observe(viewLifecycleOwner) { attitude ->
binding.apply {
artificialHorizon.setAttitude(attitude.pitch, attitude.roll)
tvPitch.text = String.format("%.2f°", attitude.pitch)
tvRoll.text = String.format("%.2f°", attitude.roll)
tvYaw.text = String.format("%.2f°", attitude.yaw)
}
viewModel.attitudeData.observe(viewLifecycleOwner) { attitude ->
binding.apply {
artificialHorizon.setAttitude(attitude.pitch, attitude.roll)
tvPitch.text = String.format("%.2f°", attitude.pitch)
tvRoll.text = String.format("%.2f°", attitude.roll)
tvYaw.text = String.format("%.2f°", attitude.yaw)
}
}
viewModel.isRecording.observe(viewLifecycleOwner) { isRecording ->
binding.apply {
btnRecord.isEnabled = !isRecording
btnStopRecording.isEnabled = isRecording
tvRecordingStatus.text = if (isRecording) "Recording..." else "Idle"
}
viewModel.isRecording.observe(viewLifecycleOwner) { isRecording ->
binding.apply {
btnRecord.isEnabled = !isRecording
btnStopRecording.isEnabled = isRecording
tvRecordingStatus.text = if (isRecording) "Recording..." else "Idle"
}
} catch (e: Exception) {
Log.e(TAG, "Error in setupObservers", e)
writeCrashLog("setupObservers", e)
}
}
private fun setupListeners() {
binding.btnRecord.setOnClickListener {
try {
startRecording()
} catch (e: Exception) {
Log.e(TAG, "Error in btnRecord click", e)
writeCrashLog("btnRecord", e)
}
}
binding.btnStopRecording.setOnClickListener {
try {
stopRecording()
} catch (e: Exception) {
Log.e(TAG, "Error in btnStop click", e)
writeCrashLog("btnStop", e)
}
}
binding.btnRecord.setOnClickListener { startRecording() }
binding.btnStopRecording.setOnClickListener { stopRecording() }
}
private fun startRecording() {
val service = collectionService
if (service != null) {
if (collectionService != null) {
viewModel.startRecording()
recordingFile = repository.createRecordingFile()
Toast.makeText(requireContext(), "Recording started", Toast.LENGTH_SHORT).show()
@@ -320,22 +268,6 @@ class DashboardFragment : Fragment() {
Toast.makeText(requireContext(), "Recording saved", Toast.LENGTH_SHORT).show()
}
private fun writeCrashLog(location: String, e: Exception) {
try {
val logFile = java.io.File(
requireContext().getExternalFilesDir(null),
"sensTools_crash.log"
)
FileWriter(logFile, true).use { writer ->
writer.appendLine("=== Crash at $location ===")
writer.appendLine("Time: ${System.currentTimeMillis()}")
writer.appendLine("Exception: ${e.javaClass.name}: ${e.message}")
e.stackTrace?.forEach { writer.appendLine(" at $it") }
writer.appendLine()
}
} catch (_: Exception) { }
}
override fun onDestroyView() {
super.onDestroyView()
if (serviceBound) {