package com.openai.codexd import android.Manifest import android.app.Activity import android.app.agent.AgentManager import android.app.agent.AgentSessionInfo import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.net.LocalSocket import android.os.Binder import android.os.Build import android.os.Bundle import android.util.Log import android.view.View import android.widget.Button import android.widget.EditText import android.widget.TableLayout import android.widget.TableRow import android.widget.TextView import android.widget.Toast import org.json.JSONArray import org.json.JSONObject import java.io.BufferedInputStream import java.io.File import java.io.IOException import java.nio.charset.StandardCharsets import java.util.Locale import kotlin.concurrent.thread class MainActivity : Activity() { companion object { private const val TAG = "CodexMainActivity" private const val ACTION_DEBUG_START_AGENT_SESSION = "com.openai.codexd.action.DEBUG_START_AGENT_SESSION" private const val ACTION_DEBUG_CANCEL_ALL_AGENT_SESSIONS = "com.openai.codexd.action.DEBUG_CANCEL_ALL_AGENT_SESSIONS" private const val EXTRA_DEBUG_PROMPT = "prompt" private const val EXTRA_DEBUG_TARGET_PACKAGE = "targetPackage" } @Volatile private var isAuthenticated = false @Volatile private var isServiceRunning = false @Volatile private var statusRefreshInFlight = false @Volatile private var agentRefreshInFlight = false @Volatile private var latestAgentRuntimeStatus: AgentCodexAppServerClient.RuntimeStatus? = null private val agentSessionController by lazy { AgentSessionController(this) } private val sessionUiLeaseToken = Binder() private val runtimeStatusListener = AgentCodexAppServerClient.RuntimeStatusListener { status -> latestAgentRuntimeStatus = status runOnUiThread { findViewById(R.id.agent_runtime_status).text = renderAgentRuntimeStatus() } } private val authStatusReceiver = object : BroadcastReceiver() { override fun onReceive( context: Context?, intent: Intent?, ) { if (intent?.action == CodexdForegroundService.ACTION_AUTH_STATE_CHANGED) { refreshAuthStatus() } } } private val sessionListener = object : AgentManager.SessionListener { override fun onSessionChanged(session: AgentSessionInfo) { if (focusedFrameworkSessionId == null && session.parentSessionId != null) { focusedFrameworkSessionId = session.sessionId } refreshAgentSessions() } override fun onSessionRemoved(sessionId: String, userId: Int) { if (focusedFrameworkSessionId == sessionId) { focusedFrameworkSessionId = null } refreshAgentSessions() } } private var sessionListenerRegistered = false private var authStatusReceiverRegistered = false private var focusedFrameworkSessionId: String? = null private var leasedParentSessionId: String? = null private var latestAgentSnapshot: AgentSnapshot = AgentSnapshot.unavailable override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) updatePaths() handleSessionIntent(intent) requestNotificationPermissionIfNeeded() } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) Log.i(TAG, "onNewIntent action=${intent.action}") setIntent(intent) handleSessionIntent(intent) refreshAgentSessions() } override fun onResume() { super.onResume() handleSessionIntent(intent) registerSessionListenerIfNeeded() registerAuthStatusReceiverIfNeeded() AgentCodexAppServerClient.registerRuntimeStatusListener(runtimeStatusListener) AgentCodexAppServerClient.refreshRuntimeStatusAsync(this) refreshAuthStatus() refreshAgentSessions(force = true) } override fun onPause() { AgentCodexAppServerClient.unregisterRuntimeStatusListener(runtimeStatusListener) unregisterAuthStatusReceiverIfNeeded() unregisterSessionListenerIfNeeded() updateSessionUiLease(null) super.onPause() } private fun updatePaths() { findViewById(R.id.socket_path).text = defaultSocketPath() findViewById(R.id.codex_home).text = defaultCodexHome() isServiceRunning = false latestAgentRuntimeStatus = AgentCodexAppServerClient.currentRuntimeStatus() updateAuthUi("Codexd status: unknown", false, null, emptyList()) updateAgentUi(AgentSnapshot.unavailable) } private fun handleSessionIntent(intent: Intent?) { val sessionId = intent?.getStringExtra(AgentManager.EXTRA_SESSION_ID) if (!sessionId.isNullOrEmpty()) { focusedFrameworkSessionId = sessionId } maybeStartSessionFromIntent(intent) } private fun maybeStartSessionFromIntent(intent: Intent?) { if (intent?.action == ACTION_DEBUG_CANCEL_ALL_AGENT_SESSIONS) { Log.i(TAG, "Handling debug cancel-all Agent sessions intent") thread { val result = runCatching { agentSessionController.cancelActiveSessions() } result.onFailure { err -> Log.w(TAG, "Failed to cancel Agent sessions from debug intent", err) showToast("Failed to cancel active sessions: ${err.message}") } result.onSuccess { cancelResult -> focusedFrameworkSessionId = null val cancelledCount = cancelResult.cancelledSessionIds.size val failedCount = cancelResult.failedSessionIds.size showToast("Cancelled $cancelledCount sessions, $failedCount failed") refreshAgentSessions(force = true) } } intent.action = null return } if (intent?.action != ACTION_DEBUG_START_AGENT_SESSION) { return } val prompt = intent.getStringExtra(EXTRA_DEBUG_PROMPT)?.trim().orEmpty() if (prompt.isEmpty()) { Log.w(TAG, "Ignoring debug start intent without prompt") intent.action = null return } val targetPackageOverride = intent.getStringExtra(EXTRA_DEBUG_TARGET_PACKAGE)?.trim() findViewById(R.id.agent_prompt).setText(prompt) findViewById(R.id.agent_target_package).setText(targetPackageOverride.orEmpty()) Log.i(TAG, "Handling debug start intent override=$targetPackageOverride prompt=${prompt.take(160)}") startDirectAgentSession(findViewById(R.id.agent_start_button)) intent.action = null } private fun registerSessionListenerIfNeeded() { if (sessionListenerRegistered || !agentSessionController.isAvailable()) { return } sessionListenerRegistered = runCatching { agentSessionController.registerSessionListener(mainExecutor, sessionListener) }.getOrDefault(false) } private fun unregisterSessionListenerIfNeeded() { if (!sessionListenerRegistered) { return } runCatching { agentSessionController.unregisterSessionListener(sessionListener) } sessionListenerRegistered = false } private fun registerAuthStatusReceiverIfNeeded() { if (authStatusReceiverRegistered) { return } val filter = IntentFilter(CodexdForegroundService.ACTION_AUTH_STATE_CHANGED) if (Build.VERSION.SDK_INT >= 33) { registerReceiver(authStatusReceiver, filter, RECEIVER_NOT_EXPORTED) } else { @Suppress("DEPRECATION") registerReceiver(authStatusReceiver, filter) } authStatusReceiverRegistered = true } private fun unregisterAuthStatusReceiverIfNeeded() { if (!authStatusReceiverRegistered) { return } unregisterReceiver(authStatusReceiver) authStatusReceiverRegistered = false } private fun updateSessionUiLease(parentSessionId: String?) { if (leasedParentSessionId == parentSessionId) { return } val previousParentSessionId = leasedParentSessionId if (previousParentSessionId != null) { runCatching { agentSessionController.unregisterSessionUiLease(previousParentSessionId, sessionUiLeaseToken) } leasedParentSessionId = null } if (parentSessionId != null) { val registered = runCatching { agentSessionController.registerSessionUiLease(parentSessionId, sessionUiLeaseToken) } if (registered.isSuccess) { leasedParentSessionId = parentSessionId } } } private fun requestNotificationPermissionIfNeeded() { if (Build.VERSION.SDK_INT < 33) { return } if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED ) { return } requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1001) } fun startDirectAgentSession(@Suppress("UNUSED_PARAMETER") view: View) { val targetPackageOverride = findViewById(R.id.agent_target_package).text.toString().trim() val prompt = findViewById(R.id.agent_prompt).text.toString().trim() if (prompt.isEmpty()) { showToast("Enter a prompt") return } Log.i(TAG, "startDirectAgentSession override=$targetPackageOverride prompt=${prompt.take(160)}") thread { val result = runCatching { AgentTaskPlanner.startSession( context = this, userObjective = prompt, targetPackageOverride = targetPackageOverride.ifBlank { null }, allowDetachedMode = true, sessionController = agentSessionController, requestUserInputHandler = { questions -> AgentUserInputPrompter.promptForAnswers(this, questions) }, ) } result.onFailure { err -> Log.w(TAG, "Failed to start Agent session", err) showToast("Failed to start Agent session: ${err.message}") refreshAgentSessions() } result.onSuccess { sessionStart -> Log.i( TAG, "Started Agent session parent=${sessionStart.parentSessionId} children=${sessionStart.childSessionIds}", ) focusedFrameworkSessionId = sessionStart.childSessionIds.firstOrNull() val targetSummary = sessionStart.plannedTargets.joinToString(", ") showToast("Started ${sessionStart.childSessionIds.size} Genie session(s) for $targetSummary via ${sessionStart.geniePackage}") refreshAgentSessions() } } } fun refreshAgentSessionAction(@Suppress("UNUSED_PARAMETER") view: View) { refreshAgentSessions(force = true) } fun cancelAllAgentSessions(@Suppress("UNUSED_PARAMETER") view: View) { thread { val result = runCatching { agentSessionController.cancelActiveSessions() } result.onFailure { err -> showToast("Failed to cancel active sessions: ${err.message}") } result.onSuccess { cancelResult -> focusedFrameworkSessionId = null val cancelledCount = cancelResult.cancelledSessionIds.size val failedCount = cancelResult.failedSessionIds.size if (cancelledCount == 0 && failedCount == 0) { showToast("No active framework sessions") } else if (failedCount == 0) { showToast("Cancelled $cancelledCount active sessions") } else { showToast("Cancelled $cancelledCount sessions, $failedCount failed") } refreshAgentSessions(force = true) } } } fun answerAgentQuestion(@Suppress("UNUSED_PARAMETER") view: View) { val selectedSession = latestAgentSnapshot.selectedSession if (selectedSession == null) { showToast("No active Genie session selected") return } val answerInput = findViewById(R.id.agent_answer_input) val answer = answerInput.text.toString().trim() if (answer.isEmpty()) { showToast("Enter an answer") return } thread { val result = runCatching { agentSessionController.answerQuestion( selectedSession.sessionId, answer, latestAgentSnapshot.parentSession?.sessionId, ) } result.onFailure { err -> showToast("Failed to answer question: ${err.message}") } result.onSuccess { answerInput.post { answerInput.text.clear() } showToast("Answered ${selectedSession.sessionId}") refreshAgentSessions(force = true) } } } fun attachAgentTarget(@Suppress("UNUSED_PARAMETER") view: View) { val selectedSession = latestAgentSnapshot.selectedSession if (selectedSession == null) { showToast("No detached target available") return } thread { val result = runCatching { agentSessionController.attachTarget(selectedSession.sessionId) } result.onFailure { err -> showToast("Failed to attach target: ${err.message}") } result.onSuccess { showToast("Attached target for ${selectedSession.sessionId}") refreshAgentSessions(force = true) } } } fun cancelAgentSession(@Suppress("UNUSED_PARAMETER") view: View) { val selectedSession = latestAgentSnapshot.selectedSession if (selectedSession == null) { showToast("No framework session selected") return } val sessionIdToCancel = latestAgentSnapshot.parentSession?.sessionId ?: selectedSession.sessionId thread { val result = runCatching { agentSessionController.cancelSession(sessionIdToCancel) } result.onFailure { err -> showToast("Failed to cancel session: ${err.message}") } result.onSuccess { if (focusedFrameworkSessionId == selectedSession.sessionId) { focusedFrameworkSessionId = null } showToast("Cancelled $sessionIdToCancel") refreshAgentSessions(force = true) } } } fun toggleCodexd(@Suppress("UNUSED_PARAMETER") view: View) { val intent = Intent(this, CodexdForegroundService::class.java).apply { putExtra(CodexdForegroundService.EXTRA_SOCKET_PATH, defaultSocketPath()) putExtra(CodexdForegroundService.EXTRA_CODEX_HOME, defaultCodexHome()) } if (isServiceRunning) { intent.action = CodexdForegroundService.ACTION_STOP startService(intent) isServiceRunning = false updateAuthUi("Codexd status: stopping service...", false, 0, emptyList()) AgentCodexAppServerClient.refreshRuntimeStatusAsync(this) return } intent.action = CodexdForegroundService.ACTION_START startForegroundService(intent) isServiceRunning = true updateAuthUi("Codexd status: starting service...", isAuthenticated, null, emptyList()) refreshAuthStatus() AgentCodexAppServerClient.refreshRuntimeStatusAsync(this) } fun authAction(@Suppress("UNUSED_PARAMETER") view: View) { if (isAuthenticated) { startSignOut() } else { startDeviceAuth() } } private fun startDeviceAuth() { val intent = Intent(this, CodexdForegroundService::class.java).apply { action = CodexdForegroundService.ACTION_START putExtra(CodexdForegroundService.EXTRA_SOCKET_PATH, defaultSocketPath()) putExtra(CodexdForegroundService.EXTRA_CODEX_HOME, defaultCodexHome()) } startForegroundService(intent) isServiceRunning = true updateAuthUi("Codexd status: requesting device code...", false, null, emptyList()) thread { val socketPath = defaultSocketPath() val response = runCatching { postDeviceAuthWithRetry(socketPath) } response.onFailure { err -> isServiceRunning = false updateAuthUi("Codexd status: failed (${err.message})", false, null, emptyList()) } response.onSuccess { deviceResponse -> when (deviceResponse.status) { "already_authenticated" -> { updateAuthUi("Codexd status: already authenticated", true, null, emptyList()) AgentCodexAppServerClient.refreshRuntimeStatusAsync(this, refreshToken = true) showToast("Already signed in") } "pending", "in_progress" -> { val url = deviceResponse.verificationUrl.orEmpty() val code = deviceResponse.userCode.orEmpty() updateAuthUi( "Codexd status: open $url and enter code $code", false, null, emptyList(), ) pollForAuthSuccess(socketPath) } else -> updateAuthUi( "Codexd status: ${deviceResponse.status}", false, null, emptyList(), ) } } } } private fun startSignOut() { updateAuthUi("Codexd status: signing out...", false, null, emptyList()) thread { val socketPath = defaultSocketPath() val result = runCatching { postLogoutWithRetry(socketPath) } result.onFailure { err -> updateAuthUi( "Codexd status: sign out failed (${err.message})", false, null, emptyList(), ) } result.onSuccess { showToast("Signed out") refreshAuthStatus() AgentCodexAppServerClient.refreshRuntimeStatusAsync(this) } } } private fun refreshAuthStatus() { if (statusRefreshInFlight) { return } statusRefreshInFlight = true thread { val socketPath = defaultSocketPath() val result = runCatching { fetchAuthStatusWithRetry(socketPath) } result.onFailure { err -> isServiceRunning = false updateAuthUi( "Codexd status: stopped or unavailable (${err.message})", false, null, emptyList(), ) } result.onSuccess { status -> isServiceRunning = true val message = if (status.authenticated) { val emailSuffix = status.accountEmail?.let { " ($it)" } ?: "" "Codexd status: signed in$emailSuffix" } else { "Codexd status: not signed in" } updateAuthUi(message, status.authenticated, status.clientCount, status.clients) } statusRefreshInFlight = false } } private fun refreshAgentSessions(force: Boolean = false) { if (!force && agentRefreshInFlight) { return } agentRefreshInFlight = true thread { val result = runCatching { agentSessionController.loadSnapshot(focusedFrameworkSessionId) } result.onFailure { err -> latestAgentSnapshot = AgentSnapshot.unavailable runOnUiThread { updateAgentUi(AgentSnapshot.unavailable, err.message) } } result.onSuccess { snapshot -> latestAgentSnapshot = snapshot focusedFrameworkSessionId = snapshot.selectedSession?.sessionId ?: focusedFrameworkSessionId updateAgentUi(snapshot) } agentRefreshInFlight = false } } private fun pollForAuthSuccess(socketPath: String) { val deadline = System.currentTimeMillis() + 15 * 60 * 1000 while (System.currentTimeMillis() < deadline) { val status = runCatching { fetchAuthStatusWithRetry(socketPath) }.getOrNull() if (status?.authenticated == true) { val emailSuffix = status.accountEmail?.let { " ($it)" } ?: "" updateAuthUi( "Codexd status: signed in$emailSuffix", true, status.clientCount, status.clients, ) AgentCodexAppServerClient.refreshRuntimeStatusAsync(this, refreshToken = true) showToast("Signed in") return } Thread.sleep(3000) } } private fun updateAgentUi(snapshot: AgentSnapshot, unavailableMessage: String? = null) { runOnUiThread { val statusView = findViewById(R.id.agent_status) val runtimeStatusView = findViewById(R.id.agent_runtime_status) val genieView = findViewById(R.id.agent_genie_package) val focusView = findViewById(R.id.agent_session_focus) val groupView = findViewById(R.id.agent_session_group) val questionLabel = findViewById(R.id.agent_question_label) val questionView = findViewById(R.id.agent_question) val answerInput = findViewById(R.id.agent_answer_input) val answerButton = findViewById