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 {
|
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
|
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.ui.charts.MagnetometerBarManager
|
||||||
import com.senstools.presentation.viewmodel.DashboardViewModel
|
import com.senstools.presentation.viewmodel.DashboardViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.FileWriter
|
|
||||||
|
|
||||||
class DashboardFragment : Fragment() {
|
class DashboardFragment : Fragment() {
|
||||||
|
|
||||||
@@ -54,29 +53,29 @@ class DashboardFragment : Fragment() {
|
|||||||
|
|
||||||
private val connection = object : ServiceConnection {
|
private val connection = object : ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
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 {
|
try {
|
||||||
Log.d(TAG, "onServiceConnected")
|
val ctx = requireContext().applicationContext
|
||||||
val binder = service as DataCollectionService.LocalBinder
|
ctx.startService(Intent(ctx, DataCollectionService::class.java))
|
||||||
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")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error in onServiceConnected", e)
|
Log.e(TAG, "startService failed, trying direct collection", e)
|
||||||
writeCrashLog("onServiceConnected", 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?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
@@ -90,42 +89,26 @@ class DashboardFragment : Fragment() {
|
|||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
try {
|
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
|
||||||
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error inflating layout", e)
|
|
||||||
writeCrashLog("onCreateView", e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
try {
|
|
||||||
repository = DataRepository(requireContext())
|
|
||||||
ekfAttitude = EKFAttitude()
|
|
||||||
|
|
||||||
setupCharts()
|
repository = DataRepository(requireContext())
|
||||||
setupObservers()
|
ekfAttitude = EKFAttitude()
|
||||||
setupListeners()
|
|
||||||
bindService()
|
setupCharts()
|
||||||
Log.d(TAG, "onViewCreated complete")
|
setupObservers()
|
||||||
} catch (e: Exception) {
|
setupListeners()
|
||||||
Log.e(TAG, "Error in onViewCreated", e)
|
bindService()
|
||||||
writeCrashLog("onViewCreated", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
try {
|
if (!serviceBound) {
|
||||||
if (!serviceBound) {
|
bindService()
|
||||||
bindService()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error in onResume", e)
|
|
||||||
writeCrashLog("onResume", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,172 +123,137 @@ class DashboardFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupCharts() {
|
private fun setupCharts() {
|
||||||
try {
|
accelChartManager = AccelChartManager(binding.chartAccel)
|
||||||
accelChartManager = AccelChartManager(binding.chartAccel)
|
gyroChartManager = GyroChartManager(binding.chartGyro)
|
||||||
gyroChartManager = GyroChartManager(binding.chartGyro)
|
magnetometerManager = MagnetometerBarManager(binding.chartMagnetometer)
|
||||||
magnetometerManager = MagnetometerBarManager(binding.chartMagnetometer)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error setting up charts", e)
|
|
||||||
writeCrashLog("setupCharts", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindService() {
|
private fun bindService() {
|
||||||
try {
|
try {
|
||||||
val intent = Intent(requireContext(), DataCollectionService::class.java)
|
val intent = Intent(requireContext(), DataCollectionService::class.java)
|
||||||
requireContext().bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
requireContext().bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||||
Log.d(TAG, "bindService called")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error binding service", e)
|
Log.e(TAG, "bindService failed", e)
|
||||||
writeCrashLog("bindService", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeServiceData() {
|
private fun observeServiceData() {
|
||||||
val service = collectionService ?: return
|
val service = collectionService ?: return
|
||||||
|
|
||||||
try {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
launch {
|
||||||
launch {
|
service.currentGPSData.collect { gps ->
|
||||||
service.currentGPSData.collect { gps ->
|
viewModel.updateGPSData(gps)
|
||||||
viewModel.updateGPSData(gps)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
launch {
|
}
|
||||||
service.currentSensorData.collect { sensor ->
|
launch {
|
||||||
viewModel.updateSensorData(sensor)
|
service.currentSensorData.collect { sensor ->
|
||||||
|
viewModel.updateSensorData(sensor)
|
||||||
|
|
||||||
// EKF update
|
if (!ekfInitialized) {
|
||||||
if (!ekfInitialized) {
|
ekfAttitude.initializeFromSensors(
|
||||||
ekfAttitude.initializeFromSensors(
|
|
||||||
sensor.accelerometer,
|
|
||||||
sensor.magnetometer
|
|
||||||
)
|
|
||||||
ekfInitialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
ekfAttitude.predict(
|
|
||||||
sensor.gyroscope,
|
|
||||||
sensor.timestamp
|
|
||||||
)
|
|
||||||
ekfAttitude.update(
|
|
||||||
sensor.accelerometer,
|
sensor.accelerometer,
|
||||||
sensor.magnetometer
|
sensor.magnetometer
|
||||||
)
|
)
|
||||||
|
ekfInitialized = true
|
||||||
|
}
|
||||||
|
|
||||||
val attitude = ekfAttitude.getAttitude()
|
ekfAttitude.predict(
|
||||||
viewModel.updateAttitudeData(attitude)
|
sensor.gyroscope,
|
||||||
|
sensor.timestamp
|
||||||
|
)
|
||||||
|
ekfAttitude.update(
|
||||||
|
sensor.accelerometer,
|
||||||
|
sensor.magnetometer
|
||||||
|
)
|
||||||
|
|
||||||
// Update charts
|
val attitude = ekfAttitude.getAttitude()
|
||||||
accelChartManager.addData(
|
viewModel.updateAttitudeData(attitude)
|
||||||
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 recording, buffer the sensor data
|
accelChartManager.addData(
|
||||||
if (viewModel.isRecording.value == true && recordingFile != null) {
|
sensor.accelerometer[0],
|
||||||
repository.appendSensorData(sensor, recordingFile!!)
|
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() {
|
private fun setupObservers() {
|
||||||
try {
|
viewModel.gpsData.observe(viewLifecycleOwner) { gps ->
|
||||||
viewModel.gpsData.observe(viewLifecycleOwner) { gps ->
|
binding.apply {
|
||||||
binding.apply {
|
tvLongitude.text = String.format("%.8f", gps.longitude)
|
||||||
tvLongitude.text = String.format("%.8f", gps.longitude)
|
tvLatitude.text = String.format("%.8f", gps.latitude)
|
||||||
tvLatitude.text = String.format("%.8f", gps.latitude)
|
tvAltitude.text = String.format("%.2f m", gps.altitude)
|
||||||
tvAltitude.text = String.format("%.2f m", gps.altitude)
|
tvSpeed.text = String.format("%.2f m/s", gps.speed)
|
||||||
tvSpeed.text = String.format("%.2f m/s", gps.speed)
|
tvBearing.text = String.format("%.2f°", gps.bearing)
|
||||||
tvBearing.text = String.format("%.2f°", gps.bearing)
|
tvSatellites.text = gps.satellites.toString()
|
||||||
tvSatellites.text = gps.satellites.toString()
|
|
||||||
|
|
||||||
if (viewModel.isRecording.value == true && recordingFile != null) {
|
if (viewModel.isRecording.value == true && recordingFile != null) {
|
||||||
repository.appendGPSData(gps, recordingFile!!)
|
repository.appendGPSData(gps, recordingFile!!)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.sensorData.observe(viewLifecycleOwner) { sensor ->
|
viewModel.sensorData.observe(viewLifecycleOwner) { sensor ->
|
||||||
binding.apply {
|
binding.apply {
|
||||||
tvAccelX.text = String.format("%.2f", sensor.accelerometer[0])
|
tvAccelX.text = String.format("%.2f", sensor.accelerometer[0])
|
||||||
tvAccelY.text = String.format("%.2f", sensor.accelerometer[1])
|
tvAccelY.text = String.format("%.2f", sensor.accelerometer[1])
|
||||||
tvAccelZ.text = String.format("%.2f", sensor.accelerometer[2])
|
tvAccelZ.text = String.format("%.2f", sensor.accelerometer[2])
|
||||||
tvGyroX.text = String.format("%.2f", sensor.gyroscope[0])
|
tvGyroX.text = String.format("%.2f", sensor.gyroscope[0])
|
||||||
tvGyroY.text = String.format("%.2f", sensor.gyroscope[1])
|
tvGyroY.text = String.format("%.2f", sensor.gyroscope[1])
|
||||||
tvGyroZ.text = String.format("%.2f", sensor.gyroscope[2])
|
tvGyroZ.text = String.format("%.2f", sensor.gyroscope[2])
|
||||||
tvMagX.text = String.format("%.2f", sensor.magnetometer[0])
|
tvMagX.text = String.format("%.2f", sensor.magnetometer[0])
|
||||||
tvMagY.text = String.format("%.2f", sensor.magnetometer[1])
|
tvMagY.text = String.format("%.2f", sensor.magnetometer[1])
|
||||||
tvMagZ.text = String.format("%.2f", sensor.magnetometer[2])
|
tvMagZ.text = String.format("%.2f", sensor.magnetometer[2])
|
||||||
tvPressure.text = String.format("%.2f m", sensor.pressureAltitude)
|
tvPressure.text = String.format("%.2f m", sensor.pressureAltitude)
|
||||||
tvTemperature.text = String.format("%.2f°C", sensor.temperature)
|
tvTemperature.text = String.format("%.2f°C", sensor.temperature)
|
||||||
tvHumidity.text = String.format("%.1f%%", sensor.humidity)
|
tvHumidity.text = String.format("%.1f%%", sensor.humidity)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.attitudeData.observe(viewLifecycleOwner) { attitude ->
|
viewModel.attitudeData.observe(viewLifecycleOwner) { attitude ->
|
||||||
binding.apply {
|
binding.apply {
|
||||||
artificialHorizon.setAttitude(attitude.pitch, attitude.roll)
|
artificialHorizon.setAttitude(attitude.pitch, attitude.roll)
|
||||||
tvPitch.text = String.format("%.2f°", attitude.pitch)
|
tvPitch.text = String.format("%.2f°", attitude.pitch)
|
||||||
tvRoll.text = String.format("%.2f°", attitude.roll)
|
tvRoll.text = String.format("%.2f°", attitude.roll)
|
||||||
tvYaw.text = String.format("%.2f°", attitude.yaw)
|
tvYaw.text = String.format("%.2f°", attitude.yaw)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.isRecording.observe(viewLifecycleOwner) { isRecording ->
|
viewModel.isRecording.observe(viewLifecycleOwner) { isRecording ->
|
||||||
binding.apply {
|
binding.apply {
|
||||||
btnRecord.isEnabled = !isRecording
|
btnRecord.isEnabled = !isRecording
|
||||||
btnStopRecording.isEnabled = isRecording
|
btnStopRecording.isEnabled = isRecording
|
||||||
tvRecordingStatus.text = if (isRecording) "Recording..." else "Idle"
|
tvRecordingStatus.text = if (isRecording) "Recording..." else "Idle"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error in setupObservers", e)
|
|
||||||
writeCrashLog("setupObservers", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupListeners() {
|
private fun setupListeners() {
|
||||||
binding.btnRecord.setOnClickListener {
|
binding.btnRecord.setOnClickListener { startRecording() }
|
||||||
try {
|
binding.btnStopRecording.setOnClickListener { stopRecording() }
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startRecording() {
|
private fun startRecording() {
|
||||||
val service = collectionService
|
if (collectionService != null) {
|
||||||
if (service != null) {
|
|
||||||
viewModel.startRecording()
|
viewModel.startRecording()
|
||||||
recordingFile = repository.createRecordingFile()
|
recordingFile = repository.createRecordingFile()
|
||||||
Toast.makeText(requireContext(), "Recording started", Toast.LENGTH_SHORT).show()
|
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()
|
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() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
if (serviceBound) {
|
if (serviceBound) {
|
||||||
|
|||||||
Reference in New Issue
Block a user