fix(android): crash on back-while-streaming (UAF) + Material You theme & connect polish
apple / swift (push) Successful in 54s
ci / rust (push) Successful in 1m37s
ci / web (push) Successful in 32s
ci / docs-site (push) Successful in 35s
android / android (push) Successful in 4m23s
deb / build-publish (push) Successful in 2m37s
decky / build-publish (push) Successful in 24s
ci / bench (push) Successful in 4m27s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m45s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 3m21s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 23s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m28s
docker / deploy-docs (push) Has been skipped
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m30s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m52s
apple / swift (push) Successful in 54s
ci / rust (push) Successful in 1m37s
ci / web (push) Successful in 32s
ci / docs-site (push) Successful in 35s
android / android (push) Successful in 4m23s
deb / build-publish (push) Successful in 2m37s
decky / build-publish (push) Successful in 24s
ci / bench (push) Successful in 4m27s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m45s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 3m21s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 23s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m28s
docker / deploy-docs (push) Has been skipped
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m30s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m52s
Crash: DisposableEffect.onDispose called nativeClose(handle) (Box::from_raw frees the SessionHandle) while the SurfaceView's surfaceDestroyed independently called nativeStopVideo/Audio/Mic on the same handle -- whichever ran after the close dereferenced freed memory (SIGSEGV: the consistent back-navigation crash). Add a one-shot `closed` guard: onDispose marks it before freeing; surfaceDestroyed skips the native calls once closed (backgrounding still stops the threads when it wins). Polish: - Branded Material You theme (Theme.kt): dynamic colour on Android 12+, punktfunk brand violets as the pre-12 fallback, replacing the generic darkColorScheme(). - ConnectScreen: "Connecting..." was rendered in error-red with no spinner; now a neutral spinner while connecting, red reserved for actual errors. Verified locally: ./gradlew :app:assembleDebug BUILD SUCCESSFUL (both ABIs + the Compose changes), debug APK assembles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
@@ -32,6 +33,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -207,12 +209,32 @@ fun ConnectScreen(settings: Settings, onConnected: (Long) -> Unit) {
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
status?.let {
|
||||
// While connecting it's progress (spinner, neutral); otherwise it's a
|
||||
// result/error (red). Previously every status showed in error-red, so a
|
||||
// normal "Connecting…" looked like a failure.
|
||||
if (connecting) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
Text(
|
||||
it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,7 @@ import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.unom.punktfunk.kit.Gamepad
|
||||
import io.unom.punktfunk.kit.Keymap
|
||||
@@ -38,7 +36,7 @@ class MainActivity : ComponentActivity() {
|
||||
navigationBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT),
|
||||
)
|
||||
setContent {
|
||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||
PunktfunkTheme {
|
||||
Surface(modifier = Modifier.fillMaxSize()) { App() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import androidx.core.view.WindowInsetsControllerCompat
|
||||
import io.unom.punktfunk.kit.Gamepad
|
||||
import io.unom.punktfunk.kit.GamepadFeedback
|
||||
import io.unom.punktfunk.kit.NativeBridge
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
@@ -70,6 +71,12 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
// One-shot teardown guard. Both the SurfaceView callback and DisposableEffect tear down on the
|
||||
// way out, but `nativeClose` frees the handle — so once it's closed, NO path may touch the handle
|
||||
// again (use-after-free → SIGSEGV: the consistent back-while-streaming crash). Both run on the
|
||||
// main thread, so a plain flag is race-free; AtomicBoolean just makes the intent explicit.
|
||||
val closed = remember { AtomicBoolean(false) }
|
||||
|
||||
DisposableEffect(handle) {
|
||||
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
controller?.let {
|
||||
@@ -81,6 +88,7 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
||||
// Host→client feedback (rumble + DualSense lightbar/LEDs); poll threads stopped before close.
|
||||
val feedback = GamepadFeedback(handle).also { it.start() }
|
||||
onDispose {
|
||||
closed.set(true) // from here the handle gets freed; surfaceDestroyed must not touch it
|
||||
feedback.stop() // stop + join the poll threads BEFORE nativeClose frees the handle
|
||||
activity?.axisMapper?.reset() // release-all so nothing sticks on the host
|
||||
activity?.axisMapper = null
|
||||
@@ -112,10 +120,16 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
// Surface gone (backgrounding, or on the way out). Stop the threads that
|
||||
// render to it — but only while the session is still open. Once
|
||||
// DisposableEffect has closed it, the handle is freed; dereferencing it
|
||||
// here is the use-after-free that crashed on back-navigation.
|
||||
if (!closed.get()) {
|
||||
NativeBridge.nativeStopMic(handle)
|
||||
NativeBridge.nativeStopAudio(handle)
|
||||
NativeBridge.nativeStopVideo(handle)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package io.unom.punktfunk
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
// punktfunk brand violets (from the app icon: #6C5BF3 / #A79FF8 / #D2C9FB on a #16132A indigo).
|
||||
// Used as the fallback dark scheme on pre-Android-12 devices; on 12+ we defer to Material You.
|
||||
private val BrandDark = darkColorScheme(
|
||||
primary = Color(0xFFA79FF8),
|
||||
onPrimary = Color(0xFF1B1442),
|
||||
primaryContainer = Color(0xFF4C3FB3),
|
||||
onPrimaryContainer = Color(0xFFE5E0FF),
|
||||
secondary = Color(0xFFC8C2EC),
|
||||
onSecondary = Color(0xFF2E2A4D),
|
||||
tertiary = Color(0xFF8FD0E8),
|
||||
onTertiary = Color(0xFF053543),
|
||||
background = Color(0xFF131129),
|
||||
onBackground = Color(0xFFE5E1F2),
|
||||
surface = Color(0xFF1A1733),
|
||||
onSurface = Color(0xFFE5E1F2),
|
||||
surfaceVariant = Color(0xFF2A2647),
|
||||
onSurfaceVariant = Color(0xFFC7C2DE),
|
||||
)
|
||||
|
||||
/**
|
||||
* App theme — always dark (a streaming client reads best on a dark canvas, and the immersive
|
||||
* stream view assumes it), but uses **Material You** dynamic colour on Android 12+ so the UI
|
||||
* harmonises with the user's wallpaper, falling back to the punktfunk brand violets below that.
|
||||
*/
|
||||
@Composable
|
||||
fun PunktfunkTheme(content: @Composable () -> Unit) {
|
||||
val scheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
dynamicDarkColorScheme(LocalContext.current)
|
||||
} else {
|
||||
BrandDark
|
||||
}
|
||||
MaterialTheme(colorScheme = scheme, content = content)
|
||||
}
|
||||
Reference in New Issue
Block a user