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.
This commit is contained in:
jpolo1224
2026-08-23 10:49:41 -04:00
parent 6451202102
commit bc18907e65
2 changed files with 73 additions and 6 deletions
@@ -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)
@@ -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