diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index c629d41..91f0c89 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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" diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/AgentTabSelectionPolicy.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/AgentTabSelectionPolicy.kt new file mode 100644 index 0000000..af5886f --- /dev/null +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/AgentTabSelectionPolicy.kt @@ -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 +} diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/MainActivity.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/MainActivity.kt index 49f6408..770c848 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/MainActivity.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/MainActivity.kt @@ -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" } } diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt index 4d9c000..e4f589c 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/automation/GoAutoAccessibilityService.kt @@ -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 diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt index d8fb337..8a1ddd1 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt @@ -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( diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/AgentTabSelectionPolicyTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/AgentTabSelectionPolicyTest.kt new file mode 100644 index 0000000..25ca839 --- /dev/null +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/AgentTabSelectionPolicyTest.kt @@ -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), + ) + } +} diff --git a/docs/02-architecture-and-code-map.md b/docs/02-architecture-and-code-map.md index 34850a5..5c918c2 100644 --- a/docs/02-architecture-and-code-map.md +++ b/docs/02-architecture-and-code-map.md @@ -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 # 架构与代码地图 @@ -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) diff --git a/docs/04-local-development-and-verification.md b/docs/04-local-development-and-verification.md index 8e588fd..738269e 100644 --- a/docs/04-local-development-and-verification.md +++ b/docs/04-local-development-and-verification.md @@ -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 # 本地开发与验证 @@ -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,前台切换序号变化也会取消本次返回。