fix: move startForeground to onStartCommand only, use applicationContext
Build APK / build (push) Failing after 4s
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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+114
-182
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user