fix(#128): preserve tab on automatic return

This commit is contained in:
QiuSW
2026-08-28 14:40:53 +08:00
parent 0527bcaae9
commit 1ec21c3b86
8 changed files with 123 additions and 25 deletions
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "cn.ilapage.goauto.agent"
minSdk = 23
targetSdk = 34
versionCode = 28
versionName = "0.9.15"
versionCode = 29
versionName = "0.9.16"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -0,0 +1,22 @@
package cn.ilapage.goauto.agent
internal object AgentTabSelectionPolicy {
const val TAB_STATUS = 1
const val TAB_COLLECTION = 2
const val TAB_PURCHASE = 3
const val TAB_SETTINGS = 4
private val validTabs = setOf(TAB_STATUS, TAB_COLLECTION, TAB_PURCHASE, TAB_SETTINGS)
fun initialTab(showStatus: Boolean, savedTab: Int?, persistedTab: Int?): Int = when {
showStatus -> TAB_STATUS
savedTab in validTabs -> requireNotNull(savedTab)
persistedTab in validTabs -> requireNotNull(persistedTab)
else -> TAB_STATUS
}
fun tabForNewIntent(currentTab: Int, showStatus: Boolean): Int =
if (showStatus) TAB_STATUS else normalize(currentTab)
fun normalize(tab: Int): Int = tab.takeIf(validTabs::contains) ?: TAB_STATUS
}
@@ -20,6 +20,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView
class MainActivity : AppCompatActivity() {
private var selectedTab = TAB_STATUS
private lateinit var navigation: BottomNavigationView
private val tabPreferences by lazy { getSharedPreferences(TAB_PREFERENCES, MODE_PRIVATE) }
private val screenPolicyHandler = android.os.Handler(android.os.Looper.getMainLooper())
private val screenPolicyRefresh = object : Runnable {
override fun run() {
@@ -30,11 +31,11 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
selectedTab = if (intent.getBooleanExtra(EXTRA_SHOW_STATUS, false)) {
TAB_STATUS
} else {
savedInstanceState?.getInt(STATE_SELECTED_TAB) ?: TAB_STATUS
}
selectedTab = AgentTabSelectionPolicy.initialTab(
showStatus = intent.getBooleanExtra(EXTRA_SHOW_STATUS, false),
savedTab = savedInstanceState?.getInt(STATE_SELECTED_TAB),
persistedTab = tabPreferences.getInt(LAST_SELECTED_TAB, TAB_STATUS),
)
setContentView(buildContent())
requestNotificationPermission()
if (AgentSettingsStore(this).serverUrl().isNotBlank()) AgentForegroundService.start(this)
@@ -43,7 +44,11 @@ class MainActivity : AppCompatActivity() {
override fun onNewIntent(intent: android.content.Intent) {
super.onNewIntent(intent)
setIntent(intent)
if (intent.getBooleanExtra(EXTRA_SHOW_STATUS, false)) navigation.selectedItemId = TAB_STATUS
val targetTab = AgentTabSelectionPolicy.tabForNewIntent(
selectedTab,
intent.getBooleanExtra(EXTRA_SHOW_STATUS, false),
)
if (targetTab != selectedTab) navigation.selectedItemId = targetTab
}
override fun onResume() {
@@ -96,15 +101,16 @@ class MainActivity : AppCompatActivity() {
}
private fun showTab(tabId: Int) {
selectedTab = tabId
val tag = "agent-tab-$tabId"
selectedTab = AgentTabSelectionPolicy.normalize(tabId)
tabPreferences.edit().putInt(LAST_SELECTED_TAB, selectedTab).apply()
val tag = "agent-tab-$selectedTab"
val transaction = supportFragmentManager.beginTransaction()
supportFragmentManager.fragments.forEach(transaction::hide)
val existing = supportFragmentManager.findFragmentByTag(tag)
if (existing != null) {
transaction.show(existing)
} else {
transaction.add(R.id.agent_tab_content, fragmentFor(tabId), tag)
transaction.add(R.id.agent_tab_content, fragmentFor(selectedTab), tag)
}
transaction.commit()
}
@@ -136,10 +142,12 @@ class MainActivity : AppCompatActivity() {
companion object {
const val EXTRA_SHOW_STATUS = "cn.ilapage.goauto.agent.SHOW_STATUS"
private const val SCREEN_POLICY_REFRESH_MILLIS = 1_000L
const val TAB_STATUS = 1
private const val TAB_COLLECTION = 2
private const val TAB_PURCHASE = 3
private const val TAB_SETTINGS = 4
const val TAB_STATUS = AgentTabSelectionPolicy.TAB_STATUS
private const val TAB_COLLECTION = AgentTabSelectionPolicy.TAB_COLLECTION
private const val TAB_PURCHASE = AgentTabSelectionPolicy.TAB_PURCHASE
private const val TAB_SETTINGS = AgentTabSelectionPolicy.TAB_SETTINGS
private const val TAB_PREFERENCES = "goauto_agent_ui"
private const val LAST_SELECTED_TAB = "last_selected_tab"
private const val STATE_SELECTED_TAB = "selected_tab"
}
}
@@ -111,11 +111,15 @@ class GoAutoAccessibilityService : AccessibilityService(), UiDriver, PddCollecto
pause = Thread::sleep,
).restore(packageName, timeoutMillis)
fun openAgentStatus(): Boolean = runCatching {
fun openAgentStatus(): Boolean = openAgent(showStatus = true)
fun openAgentPreservingTab(): Boolean = openAgent(showStatus = false)
private fun openAgent(showStatus: Boolean): Boolean = runCatching {
startActivity(
android.content.Intent(this, cn.ilapage.goauto.agent.MainActivity::class.java).apply {
addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP or android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP)
putExtra(cn.ilapage.goauto.agent.MainActivity.EXTRA_SHOW_STATUS, true)
if (showStatus) putExtra(cn.ilapage.goauto.agent.MainActivity.EXTRA_SHOW_STATUS, true)
},
)
true
@@ -846,7 +846,7 @@ class AgentForegroundService : Service() {
}
private fun requestReturnToAgent(accessibility: GoAutoAccessibilityService) {
val launched = accessibility.openAgentStatus()
val launched = accessibility.openAgentPreservingTab()
if (launched) SystemClock.sleep(RETURN_CONFIRM_DELAY_MILLIS)
val returned = launched && accessibility.currentPackage() == packageName
finishIdleReturn(
@@ -0,0 +1,63 @@
package cn.ilapage.goauto.agent
import org.junit.Assert.assertEquals
import org.junit.Test
class AgentTabSelectionPolicyTest {
@Test
fun `explicit status target overrides saved and persisted tabs`() {
assertEquals(
AgentTabSelectionPolicy.TAB_STATUS,
AgentTabSelectionPolicy.initialTab(
showStatus = true,
savedTab = AgentTabSelectionPolicy.TAB_COLLECTION,
persistedTab = AgentTabSelectionPolicy.TAB_PURCHASE,
),
)
}
@Test
fun `saved tab wins when activity is recreated`() {
assertEquals(
AgentTabSelectionPolicy.TAB_SETTINGS,
AgentTabSelectionPolicy.initialTab(
showStatus = false,
savedTab = AgentTabSelectionPolicy.TAB_SETTINGS,
persistedTab = AgentTabSelectionPolicy.TAB_COLLECTION,
),
)
}
@Test
fun `persisted tab restores after process recreation`() {
assertEquals(
AgentTabSelectionPolicy.TAB_COLLECTION,
AgentTabSelectionPolicy.initialTab(
showStatus = false,
savedTab = null,
persistedTab = AgentTabSelectionPolicy.TAB_COLLECTION,
),
)
}
@Test
fun `missing or invalid history falls back to status`() {
assertEquals(
AgentTabSelectionPolicy.TAB_STATUS,
AgentTabSelectionPolicy.initialTab(false, null, 999),
)
assertEquals(AgentTabSelectionPolicy.TAB_STATUS, AgentTabSelectionPolicy.normalize(999))
}
@Test
fun `automatic return keeps current tab while explicit intent opens status`() {
assertEquals(
AgentTabSelectionPolicy.TAB_COLLECTION,
AgentTabSelectionPolicy.tabForNewIntent(AgentTabSelectionPolicy.TAB_COLLECTION, false),
)
assertEquals(
AgentTabSelectionPolicy.TAB_STATUS,
AgentTabSelectionPolicy.tabForNewIntent(AgentTabSelectionPolicy.TAB_COLLECTION, true),
)
}
}
+4 -4
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Architecture-and-Code-Map
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Architecture-and-Code-Map.-
wiki_revision: 340505ae259dc7bfae74f991e0fb72ad0daeabe9
synchronized_at: 2026-08-28T06:14:21Z
wiki_revision: 5039f107d1f61cf5290eae38719a305b41fb35cc
synchronized_at: 2026-08-28T06:34:33Z
<!-- gitea-wiki-mirror:end -->
# 架构与代码地图
@@ -152,7 +152,7 @@ Android Portal/Agent
## Android Agent 0.3 设备端外壳(#88)
- Android 入口仍为单 Activity,但从动态单页升级为 AndroidX Fragment + Material `BottomNavigationView` 的四 Tab 外壳:状态、采集、采购、设置;默认进入状态页并保存当前 Tab。
- Android 入口仍为单 Activity,但从动态单页升级为 AndroidX Fragment + Material `BottomNavigationView` 的四 Tab 外壳:状态、采集、采购、设置;首次启动默认进入状态页,并持久保存最近一次有效 Tab。
- `android/app/src/main/java/cn/ilapage/goauto/agent/ui/` 保存状态页、设置页、任务占位页、无障碍真实就绪检测和共享 UI 组件;采集/采购历史列表与详情由 #90 接入。
- 状态页同时读取系统启用的 AccessibilityService 组件和 `GoAutoAccessibilityService.instance` 绑定事实,区分“未开启”“已开启等待连接”“已开启并就绪”,并显示服务连接、当前任务和最近心跳。
- 设置页维护服务器地址和设备名称,测试连接只访问 `GET /api/v1/health`;保存后沿用当前 installId / Device Token 重新连接。执行任务或测试连接期间表单与按钮禁用。
@@ -164,7 +164,7 @@ Android Portal/Agent
- `AgentForegroundService` 在采集结果已提交,或采购结果与 Outbox 已在同一事务安全保存后,建立 5 秒任务结束冷却;不新增第二套队列轮询器。
- 冷却到期仍由现有任务轮询按“采购优先、再采集”依次确认两个队列都为空。任何新任务、网络/服务端异常或本地未解除的不可逆采购边界都会取消返回。
- `IdleReturnCoordinator` 只保存冷却时间、自动化前台包名与无障碍前台切换序号,不保存控件树或截图。用户切换到其他 App 后,即使又返回 PDD,也不会被强制拉回 Agent。
- 返回请求只允许从 Agent 本次自动化停留的 PDD 或受支持浏览器页面发起,并由无障碍服务打开 `MainActivity` 状态 Tab;Android 后台 Activity 启动限制导致失败时只提示手动打开,不重复强拉。
- 返回请求只允许从 Agent 本次自动化停留的 PDD 或受支持浏览器页面发起,并由无障碍服务打开 `MainActivity`:现有 Activity 保持当前 Tab,重建时恢复最近一次有效 Tab;Android 后台 Activity 启动限制导致失败时只提示手动打开,不重复强拉。
- 任务执行和冷却分别使用带超时的亮屏锁;`MainActivity` 仅在运行态标记要求时设置 `FLAG_KEEP_SCREEN_ON`。任务、新任务接管、取消、返回或服务销毁后统一释放,不绕过设备 PIN、图案或密码。
## Agent 任务历史(#90)
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Local-Development-and-Verification
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Local-Development-and-Verification.-
wiki_revision: bc940a7a8eaf2aa785ece5f031ae81aa6c217a4b
synchronized_at: 2026-08-28T06:14:26Z
wiki_revision: 85b880bba09a4276321cd4eb4eb64d07d6f64ec9
synchronized_at: 2026-08-28T06:34:38Z
<!-- gitea-wiki-mirror:end -->
# 本地开发与验证
@@ -253,7 +253,8 @@ adb shell am start -n cn.ilapage.goauto.agent/.MainActivity
真机验证应至少覆盖:
- 完成一个不会支付的采集或演练任务,确认结果已提交后约 5 秒返回 Agent“状态”Tab;记录 ROM 是否限制后台 Activity 启动。
- 分别在采集、采购和设置 Tab 离开 Agent,完成一个不会支付的采集或演练任务,确认结果已提交后约 5 秒返回 Agent,并保持离开前 Tab;记录 ROM 是否限制后台 Activity 启动。
- 在最近 Tab 不是状态页时重建 MainActivity 或 Agent 进程,确认恢复最近有效 Tab;无历史或无效值回退状态页,系统辅助功能按钮仍显式打开状态页。
- 冷却 5 秒内创建任一类型新任务,确认 Agent 取消返回并继续按采购优先、采集其次执行。
- 冷却期间断网,确认不会把网络失败当成空队列,也不会返回。
- 冷却期间人工切换到其他 App,确认 Agent 不会强拉回;即使再切回 PDD,前台切换序号变化也会取消本次返回。