Widget trampoline waits for foreground recording; fixes 10s notification delay
This commit is contained in:
parent
1f46d2a0e4
commit
673330293a
1 changed files with 48 additions and 2 deletions
|
|
@ -6,6 +6,14 @@ import android.os.Bundle
|
||||||
import com.shonar.MainActivity
|
import com.shonar.MainActivity
|
||||||
import com.shonar.recording.RecordingPermissionHelper
|
import com.shonar.recording.RecordingPermissionHelper
|
||||||
import com.shonar.recording.RecordingService
|
import com.shonar.recording.RecordingService
|
||||||
|
import com.shonar.recording.RecordingSnapshot
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invisible trampoline for the widget tap. A widget broadcast into a cold
|
* Invisible trampoline for the widget tap. A widget broadcast into a cold
|
||||||
|
|
@ -17,9 +25,20 @@ import com.shonar.recording.RecordingService
|
||||||
* Forwards to RecordingService.START (or MainActivity when the mic
|
* Forwards to RecordingService.START (or MainActivity when the mic
|
||||||
* permission is missing) and finishes before the first frame draws, so
|
* permission is missing) and finishes before the first frame draws, so
|
||||||
* there is no visible flash.
|
* there is no visible flash.
|
||||||
|
*
|
||||||
|
* The trampoline stays alive (still invisible) until the service reports a
|
||||||
|
* non-IDLE snapshot or a short bound fires. Finishing instantly drops our
|
||||||
|
* uid from TOP before startForeground() lands, making the start
|
||||||
|
* background-originated — the system then defers the notification's
|
||||||
|
* buzz/heads-up by ~10s (traced on-device 2026-09-18: FGS allowed
|
||||||
|
* 17:03:17.4, first buzz 17:03:27.5). Waiting for the snapshot keeps the
|
||||||
|
* whole start foreground-originated; the wait ends the moment recording
|
||||||
|
* state flips, so it adds no artificial delay.
|
||||||
*/
|
*/
|
||||||
class StartRecordingActivity : Activity() {
|
class StartRecordingActivity : Activity() {
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if (!RecordingPermissionHelper.hasRecordAudio(this)) {
|
if (!RecordingPermissionHelper.hasRecordAudio(this)) {
|
||||||
|
|
@ -28,10 +47,37 @@ class StartRecordingActivity : Activity() {
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
.setAction(RecordingService.ACTION_START),
|
.setAction(RecordingService.ACTION_START),
|
||||||
)
|
)
|
||||||
} else {
|
finish()
|
||||||
RecordingService.command(this, RecordingService.ACTION_START)
|
return
|
||||||
}
|
}
|
||||||
|
RecordingService.command(this, RecordingService.ACTION_START)
|
||||||
ShonarWidgetUpdater.refresh(this)
|
ShonarWidgetUpdater.refresh(this)
|
||||||
|
scope.launch {
|
||||||
|
// Bound, not a delay: returns the instant recording starts; the
|
||||||
|
// timeout only covers mic-failure paths (the service reverts the
|
||||||
|
// UI itself there).
|
||||||
|
withTimeoutOrNull(TRAMPOLINE_WAIT_MS) {
|
||||||
|
RecordingService.snapshot.first {
|
||||||
|
it.phase != RecordingSnapshot.Phase.IDLE
|
||||||
|
}
|
||||||
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
// singleInstance re-tap while a waiter is pending: the first waiter
|
||||||
|
// already covers the start — never strand a second instance.
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
scope.cancel()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TRAMPOLINE_WAIT_MS = 2_500L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue