From bc18907e6505945175e5cdbb2802a1244142307c Mon Sep 17 00:00:00 2001 From: jpolo1224 Date: Sun, 23 Aug 2026 09:48:20 -0400 Subject: [PATCH] Fix Discord stuck on "Connecting" (crash loop in :discord) connect() resolves the Discord app through DiscordSocialSdkInit.getEngineActivity() and hands the result straight to Context.getPackageManager() with no null check. That static was only ever set by DiscordAuthActivity, which runs during sign-in -- so every launch with a cached token started the SDK with it null, and :discord died with an NPE. Android restarted the service, which started again and died again. The app process never saw any of it, because the protocol is poll-only: the UI just sat on "Connecting" forever. Binding the SDK only during sign-in was wrong independently of that. The SDK's statics are per-process and Android restarts :discord whenever it likes, so the binding has to be re-established on demand rather than assumed to survive. Handing the Activity over is now separate from opening the browser: DiscordAuthActivity takes an EXTRA_AUTHORIZE flag and always binds, and the service will not call start() until a binding exists, launching the invisible Activity itself when there is none. --- .../com/armsx2/discord/DiscordAuthActivity.kt | 34 +++++++++++++- .../java/com/armsx2/discord/DiscordService.kt | 45 +++++++++++++++++-- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/platforms/android/app/src/main/java/com/armsx2/discord/DiscordAuthActivity.kt b/platforms/android/app/src/main/java/com/armsx2/discord/DiscordAuthActivity.kt index 61457db905..b335868eb7 100644 --- a/platforms/android/app/src/main/java/com/armsx2/discord/DiscordAuthActivity.kt +++ b/platforms/android/app/src/main/java/com/armsx2/discord/DiscordAuthActivity.kt @@ -48,6 +48,26 @@ import android.util.Log * here; the app process learns the outcome from the next state poll like any other change. */ class DiscordAuthActivity : Activity() { + companion object { + /** Intent extra: also open the browser sign-in, rather than only handing over the Activity. */ + const val EXTRA_AUTHORIZE = "authorize" + + /** + * True once setEngineActivity has run in THIS process. + * + * The SDK keeps the Activity in a static, and statics are per-process, so this resets + * every time Android restarts :discord -- which it does freely. Anything that needs the + * SDK must check this rather than assume sign-in already happened. + */ + @Volatile + var engineBound = false + private set + + /** Ran once, when the Activity below hands itself to the SDK. Set by DiscordService. */ + @Volatile + var onEngineBound: (() -> Unit)? = null + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -65,10 +85,20 @@ class DiscordAuthActivity : Activity() { .getMethod("setEngineActivity", Activity::class.java) .invoke(null, this) } + .onSuccess { + engineBound = true + val waiting = onEngineBound + onEngineBound = null + waiting?.invoke() + } .onFailure { Log.w("ARMSX2DiscordSvc", "setEngineActivity failed: ${it.message}") } - runCatching { DiscordNative.authorize() } - .onFailure { Log.w("ARMSX2DiscordSvc", "authorize failed: ${it.message}") } + // Only when asked. Handing the Activity over is now also done on a plain start, and that + // must not drag the browser up with it. + if (intent?.getBooleanExtra(EXTRA_AUTHORIZE, false) == true) { + runCatching { DiscordNative.authorize() } + .onFailure { Log.w("ARMSX2DiscordSvc", "authorize failed: ${it.message}") } + } finish() overridePendingTransition(0, 0) diff --git a/platforms/android/app/src/main/java/com/armsx2/discord/DiscordService.kt b/platforms/android/app/src/main/java/com/armsx2/discord/DiscordService.kt index 064d30c39f..541948047a 100644 --- a/platforms/android/app/src/main/java/com/armsx2/discord/DiscordService.kt +++ b/platforms/android/app/src/main/java/com/armsx2/discord/DiscordService.kt @@ -99,10 +99,7 @@ class DiscordService : Service() { when (msg.what) { DiscordIpc.MSG_START -> { if (!loaded) return - val token = msg.data?.getString(DiscordIpc.DATA_TOKEN).orEmpty() - runCatching { DiscordNative.start(token) } - .onSuccess { started = true } - .onFailure { Log.w(TAG, "start failed: ${it.message}") } + startWhenEngineBound(msg.data?.getString(DiscordIpc.DATA_TOKEN).orEmpty()) } DiscordIpc.MSG_AUTHORIZE -> { @@ -112,6 +109,7 @@ class DiscordService : Service() { // reference the SDK here cannot use. DiscordAuthActivity exists only to be that. runCatching { val i = Intent(this, DiscordAuthActivity::class.java) + .putExtra(DiscordAuthActivity.EXTRA_AUTHORIZE, true) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(i) }.onFailure { Log.w(TAG, "auth activity failed: ${it.message}") } @@ -140,6 +138,45 @@ class DiscordService : Service() { } } + /** + * Start the SDK, but not before it has an Activity in this process. + * + * The SDK's connect() resolves the Discord app through + * DiscordSocialSdkInit.getEngineActivity() and passes the result straight to + * Context.getPackageManager() with no null check, so starting before that Activity exists + * takes :discord down with an NPE. Android then restarts the service, which starts again and + * dies again -- a crash loop the app process cannot see, because it only ever polls for state. + * The UI sat on "Connecting" forever. + * + * The Activity used to be created only by the sign-in flow, so this hit every launch that had + * a cached token -- i.e. every launch after the first. It is not specific to signing in: the + * SDK's statics are per-process and Android restarts :discord whenever it likes, so the + * binding has to be re-established on demand rather than assumed. + */ + private fun startWhenEngineBound(token: String) { + if (DiscordAuthActivity.engineBound) { + doStart(token) + return + } + DiscordAuthActivity.onEngineBound = { handler.post { doStart(token) } } + runCatching { + startActivity( + Intent(this, DiscordAuthActivity::class.java) + .putExtra(DiscordAuthActivity.EXTRA_AUTHORIZE, false) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + ) + }.onFailure { + DiscordAuthActivity.onEngineBound = null + Log.w(TAG, "could not hand the SDK an Activity: ${it.message}") + } + } + + private fun doStart(token: String) { + runCatching { DiscordNative.start(token) } + .onSuccess { started = true } + .onFailure { Log.w(TAG, "start failed: ${it.message}") } + } + /** One snapshot, on request. Everything the app's UI renders comes through here. */ private fun reply(to: Messenger?) { val target = to ?: return