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 主要有两个限制:
- 它主要用于调整同一个 task 内的 Activity 顺序。
- 如果 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 响应,并不能让支付应用退出。
通过 deep link 或 PendingIntent 返回
如果支付接口允许传入回调地址,可以为 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。
备注:内容仅供参考。
