PAYATHON 2026

Android 11 支付后如何从 Besteron 返回应用

排障赵师傅

结论

Intent.FLAG_ACTIVITY_REORDER_TO_FRONT 无法可靠地将应用从 Besteron 界面切回前台。它只能调整当前任务栈中已有 Activity 的顺序,既不能关闭其他应用的 Activity,也无法避开 Android 10 之后的后台 Activity 启动限制。

交易结束后,应由 Besteron 或其支付 SDK 通过以下方式返回商户应用:

  • 结束支付 Activity,并通过 Activity Result 将结果返回调用方。
  • 调用预先传入的 PendingIntent、callback URI 或 deep link。
  • 提供 close、dismiss 或 return-to-merchant API。

如果 Besteron 不支持这些返回机制,普通 Android 应用无权强制关闭它。商户应用只能尝试将自己的任务栈恢复到前台,或者发送通知,让用户点击后返回。对于受管控的 SUNMI 专用终端,可以考虑 moveTaskToFront() 或 Device Owner/Kiosk 方案,但这些方案依赖具体的设备策略,不适用于普通 Android 11 应用。

FLAG_ACTIVITY_REORDER_TO_FRONT 为什么无效

NEXO HTTP 响应与 Android 界面切换属于两条独立链路。收到支付响应只代表交易请求已有结果,Besteron 的 Activity 不会因此自动执行 finish()。

FLAG_ACTIVITY_REORDER_TO_FRONT 主要有两个限制:

  1. 它主要用于调整同一个 task 内的 Activity 顺序。
  2. 如果 Besteron 位于另一个 task,或者当前应用已进入后台,该 flag 既无法关闭 Besteron,也未必能让自己的 task 回到前台。

Android 11 还限制后台应用直接启动 Activity。即使 paymentResult.collect 正常执行,系统也可能只将启动请求加入任务栈,而不会立即切换前台界面。

finish()、finishAffinity() 和 NavController.navigate() 同样只能操作应用自己的 Activity 或 Fragment,不能关闭其他应用的界面。

让支付端正常返回

如果应用通过 Intent 启动支付界面,应使用 Activity Result API。支付端完成交易后,需要调用 setResult() 和 finish()。

调用方可以这样写:

private val paymentLauncher =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        if (result.resultCode == Activity.RESULT_OK) {
            viewModel.confirmTransaction()
        } else {
            viewModel.cancelTransaction()
        }
    }

private fun startPayment(paymentIntent: Intent) {
    paymentLauncher.launch(paymentIntent)
}

支付应用完成交易后应执行类似操作:

setResult(
    Activity.RESULT_OK,
    Intent().putExtra("payment_result", paymentResult)
)
finish()

当前场景通过 HTTP 请求触发 Besteron UI,因此未必能直接采用 Activity Result。需要查看 Besteron、NEXO 服务或 SUNMI 的集成文档,确认是否支持以下能力:

  • merchant callback Activity
  • return URI 或 deep link
  • Android PendingIntent
  • 支付完成后自动关闭 UI 的配置项
  • close、dismiss、abort 或 return 命令

具体参数应以供应商提供的接口为准。在商户应用中处理 HTTP 响应,并不能让支付应用退出。

如果支付接口允许传入回调地址,可以为 MainActivity 配置 deep link:

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTop">

    <intent-filter>
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data
            android:scheme="myapp"
            android:host="payment-result" />
    </intent-filter>
</activity>

支付端完成交易后打开:

myapp://payment-result?transactionId=123

MainActivity 可以在 onNewIntent() 中接收结果:

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    setIntent(intent)
    handlePaymentReturn(intent)
}

private fun handlePaymentReturn(intent: Intent) {
    if (intent.data?.host == "payment-result") {
        val transactionId = intent.data?.getQueryParameter("transactionId")
        viewModel.onPaymentReturned(transactionId)
    }
}

如果接口支持 PendingIntent,可以这样封装返回目标:

val returnIntent = Intent(this, MainActivity::class.java).apply {
    action = "com.example.app.PAYMENT_RETURN"
    flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
        Intent.FLAG_ACTIVITY_SINGLE_TOP
}

val returnPendingIntent = PendingIntent.getActivity(
    this,
    1001,
    returnIntent,
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

Besteron 或中间服务必须主动发送这个 PendingIntent,商户应用才能收到返回请求。

尝试恢复现有任务栈

如果设备允许应用回到前台,可以组合使用 CLEAR_TOP、SINGLE_TOP 和 NEW_TASK:

val intent = Intent(requireContext(), MainActivity::class.java).apply {
    action = "com.example.app.PAYMENT_RETURN"
    putExtra("payment_completed", true)

    flags = Intent.FLAG_ACTIVITY_NEW_TASK or
        Intent.FLAG_ACTIVITY_CLEAR_TOP or
        Intent.FLAG_ACTIVITY_SINGLE_TOP
}

startActivity(intent)

同时将 MainActivity 配置为 singleTop:

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTop" />

这些 flag 分别用于:

  • FLAG_ACTIVITY_NEW_TASK:查找或启动应用所属的 task。
  • FLAG_ACTIVITY_CLEAR_TOP:目标 Activity 已存在时,清理同一 task 中位于它上方的 Activity。
  • FLAG_ACTIVITY_SINGLE_TOP:复用栈顶的 MainActivity,并通过 onNewIntent() 接收请求。

这种方案只能尽量恢复现有任务栈。如果应用已经处于后台,Android 11 仍可能阻止界面立即显示,而且这些 flag 无法关闭独立 task 中的 Besteron UI。

不要使用 FLAG_ACTIVITY_CLEAR_TASK,否则应用自己的任务栈会被清空,CheckoutFragment 及其页面状态也会丢失。

专用终端可尝试 moveTaskToFront()

在企业管控或 Kiosk 设备上,可以测试 ActivityManager.moveTaskToFront():

<uses-permission android:name="android.permission.REORDER_TASKS" />
val activityManager =
    requireContext().getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager

activityManager.moveTaskToFront(
    requireActivity().taskId,
    ActivityManager.MOVE_TASK_WITH_HOME
)

这个方法移动的是商户应用自己的 task,不会关闭 Besteron。执行结果还会受到系统后台启动策略、SUNMI 固件和设备管理策略的影响。

如果应用是 Device Owner,或者终端运行在 Lock Task/Kiosk 模式,应通过设备管理配置允许商户应用与支付应用相互切换。普通应用不应依赖 Accessibility、模拟 Home/Back 键或终止 Besteron 进程。

调整 Flow 收集和导航逻辑

当前代码使用:

repeatOnLifecycle(Lifecycle.State.CREATED)

应用被 Besteron 覆盖后,Fragment 可能仍处于 CREATED 状态。支付结果会继续被处理,但商户应用的界面此时不可见。后续的 navigate() 可能已经执行,只是用户看到的仍是 Besteron。

UI 导航更适合在 STARTED 或 RESUMED 状态下执行:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.transactionResult.collect { item ->
            val navController = findNavController()

            if (navController.currentDestination?.id != R.id.checkoutFragment) {
                return@collect
            }

            val receipt = viewModel.receiptPdf.value?.let(::PdfDocument)
            val ticket = viewModel.ticketPdf.value?.let(::PdfDocument)

            navController.navigate(
                R.id.action_checkoutFragment_to_confirmationFragment,
                Bundle().apply {
                    putString("name", args.name)
                    putFloat("totalPrice", totalPrice.toFloat())
                    putString(
                        "ticketsDescription",
                        binding.summaryTicketBreakdown.text.toString()
                    )
                    putString("eventDateTime", args.date)
                    putString("orderNumber", item.toString())
                    putInt("itemType", args.itemType)
                    putBoolean("hasTimetable", args.hasTimetable)
                    putSerializable("receipt", receipt)
                    putSerializable("ticket", ticket)
                }
            )
        }
    }
}

lifecycleScope 默认在主线程收集,通常不需要额外调用 runOnUiThread()。

支付结果还应设计成一次性事件,或者按交易 ID 去重。如果 paymentResult、transactionResult 是会重放最后一个值的 StateFlow,Fragment 恢复后可能再次提交订单或重复导航。订单确认接口也应使用交易号或幂等键,避免重复处理。

无法自动返回时的兜底方式

如果供应商无法提供自动返回能力,同时 Android 11 又阻止后台界面切换,可以发送通知,让用户点击后返回应用。由用户触发的 PendingIntent 符合系统的后台启动规则:

val intent = Intent(context, MainActivity::class.java).apply {
    action = "com.example.app.PAYMENT_RETURN"
    flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
        Intent.FLAG_ACTIVITY_SINGLE_TOP
}

val pendingIntent = PendingIntent.getActivity(
    context,
    1002,
    intent,
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

val notification = NotificationCompat.Builder(context, CHANNEL_ID)
    .setSmallIcon(R.drawable.ic_notification)
    .setContentTitle("支付已完成")
    .setContentText("点击返回订单页面")
    .setContentIntent(pendingIntent)
    .setAutoCancel(true)
    .build()

NotificationManagerCompat.from(context).notify(1002, notification)

全屏 Intent 主要用于来电、闹钟等高优先级场景,不适合用来绕过普通支付完成通知的后台启动限制。

应先确认 Besteron 是否提供正式的返回协议。如果支付端始终将自己的 Activity 留在前台,商户应用只能尝试恢复自己的 task,无法从外部安全地结束该 Activity。

备注:内容仅供参考。