// Core of an Android agent app (API 26+; permissions CALL_PHONE, READ_PHONE_STATE, foreground service). // Loop: GET /api/device/jobs/next?wait=20 -> run(steps) -> POST /api/device/jobs/{order_id}/result. // Keep SIM_PIN in EncryptedSharedPreferences on the phone; substitute {pin} locally. import android.content.Context import android.os.Handler import android.os.Looper import android.telephony.TelephonyManager class UssdRunner(ctx: Context) { private val tm = ctx.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager private val main = Handler(Looper.getMainLooper()) /** outcome: "success" | "failed" | "notsent" (safe to retry on another SIM) */ fun run(code: String, done: (outcome: String, text: String) -> Unit) { tm.sendUssdRequest(code, object : TelephonyManager.UssdResponseCallback() { override fun onReceiveUssdResponse(t: TelephonyManager, req: String, resp: CharSequence) { val s = resp.toString().lowercase() val ok = listOf("success", "recharged", "completed").any { s.contains(it) } && listOf("fail", "insufficient", "invalid", "error").none { s.contains(it) } done(if (ok) "success" else "failed", resp.toString()) } override fun onReceiveUssdResponseFailed(t: TelephonyManager, req: String, failureCode: Int) { // FAILURE_CODE_UNAVAILABLE = nothing was sent -> report failed with retryable=true. // Anything else is an unknown outcome: report nothing, the server escalates to manual review. done(if (failureCode == TelephonyManager.USSD_ERROR_SERVICE_UNAVAIL) "notsent" else "unknown", "USSD failure $failureCode") } }, main) } } // Multi-step menus (reply with 1, number, amount, PIN...) need an AccessibilityService that reads and fills the USSD dialog.