fix(#128): preserve tab on automatic return
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,前台切换序号变化也会取消本次返回。
|
||||
|
||||
Reference in New Issue
Block a user