mobile-ai-agents 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +228 -0
- package/CLAUDE.md +236 -0
- package/LICENSE +21 -0
- package/README.md +652 -0
- package/agents/android/android-crash-analyzer/README.md +134 -0
- package/agents/android/android-crash-analyzer/agent.md +206 -0
- package/agents/android/android-crash-analyzer/example-input.md +138 -0
- package/agents/android/android-crash-analyzer/example-output.md +225 -0
- package/agents/android/anr-investigation/README.md +78 -0
- package/agents/android/anr-investigation/agent.md +117 -0
- package/agents/android/anr-investigation/example-input.md +62 -0
- package/agents/android/anr-investigation/example-output.md +103 -0
- package/agents/android/axiom/agent.md +197 -0
- package/agents/android/code-reviewer/README.md +108 -0
- package/agents/android/code-reviewer/agent.md +210 -0
- package/agents/android/code-reviewer/example-input.md +116 -0
- package/agents/android/code-reviewer/example-output.md +115 -0
- package/agents/android/compose-navigation/README.md +76 -0
- package/agents/android/compose-navigation/agent.md +121 -0
- package/agents/android/compose-navigation/example-input.md +49 -0
- package/agents/android/compose-navigation/example-output.md +325 -0
- package/agents/android/compose-screen-builder/README.md +95 -0
- package/agents/android/compose-screen-builder/agent.md +170 -0
- package/agents/android/compose-screen-builder/example-input.md +40 -0
- package/agents/android/compose-screen-builder/example-output.md +568 -0
- package/agents/android/compose-ui-reviewer/README.md +73 -0
- package/agents/android/compose-ui-reviewer/agent.md +253 -0
- package/agents/android/compose-ui-reviewer/example-input.md +84 -0
- package/agents/android/compose-ui-reviewer/example-output.md +102 -0
- package/agents/android/crash-analyzer/README.md +53 -0
- package/agents/android/crash-analyzer/agent.md +295 -0
- package/agents/android/memory-leak-analyzer/README.md +79 -0
- package/agents/android/memory-leak-analyzer/agent.md +116 -0
- package/agents/android/memory-leak-analyzer/example-input.md +78 -0
- package/agents/android/memory-leak-analyzer/example-output.md +128 -0
- package/agents/cross-platform/accessibility-auditor/README.md +47 -0
- package/agents/cross-platform/accessibility-auditor/agent.md +268 -0
- package/agents/cross-platform/accessibility-auditor/example-input.md +84 -0
- package/agents/cross-platform/accessibility-auditor/example-output.md +132 -0
- package/agents/cross-platform/appforge/agent.md +303 -0
- package/agents/cross-platform/ci-cd-generator/README.md +65 -0
- package/agents/cross-platform/ci-cd-generator/agent.md +422 -0
- package/agents/cross-platform/ci-cd-generator/example-input.md +69 -0
- package/agents/cross-platform/ci-cd-generator/example-output.md +261 -0
- package/agents/cross-platform/crasher/agent.md +221 -0
- package/agents/cross-platform/figma/agent.md +247 -0
- package/agents/cross-platform/launchpad/agent.md +330 -0
- package/agents/cross-platform/mobile-harness/agent.md +422 -0
- package/agents/cross-platform/mobile-memory/agent.md +429 -0
- package/agents/cross-platform/perf/agent.md +261 -0
- package/agents/cross-platform/pipeline/agent.md +288 -0
- package/agents/cross-platform/release-notes-generator/README.md +63 -0
- package/agents/cross-platform/release-notes-generator/agent.md +218 -0
- package/agents/cross-platform/release-notes-generator/example-input.md +71 -0
- package/agents/cross-platform/release-notes-generator/example-output.md +96 -0
- package/agents/cross-platform/scribe/agent.md +180 -0
- package/agents/cross-platform/security-scanner/README.md +61 -0
- package/agents/cross-platform/security-scanner/agent.md +287 -0
- package/agents/cross-platform/security-scanner/example-input.md +77 -0
- package/agents/cross-platform/security-scanner/example-output.md +141 -0
- package/agents/cross-platform/sentinel/agent.md +217 -0
- package/agents/cross-platform/store-listing-writer/README.md +58 -0
- package/agents/cross-platform/store-listing-writer/agent.md +380 -0
- package/agents/cross-platform/store-listing-writer/example-input.md +67 -0
- package/agents/cross-platform/store-listing-writer/example-output.md +131 -0
- package/agents/flutter/bloc-feature-builder/README.md +101 -0
- package/agents/flutter/bloc-feature-builder/agent.md +225 -0
- package/agents/flutter/bloc-feature-builder/example-input.md +47 -0
- package/agents/flutter/bloc-feature-builder/example-output.md +838 -0
- package/agents/flutter/dart/agent.md +223 -0
- package/agents/flutter/widget-generator/README.md +66 -0
- package/agents/flutter/widget-generator/agent.md +264 -0
- package/agents/flutter/widget-generator/example-input.md +71 -0
- package/agents/flutter/widget-generator/example-output.md +163 -0
- package/agents/gaming/forge/agent.md +197 -0
- package/agents/gaming/unreal/agent.md +200 -0
- package/agents/ios/crash-analyzer/README.md +69 -0
- package/agents/ios/crash-analyzer/agent.md +300 -0
- package/agents/ios/crash-analyzer/example-input.md +83 -0
- package/agents/ios/crash-analyzer/example-output.md +126 -0
- package/agents/ios/swift/agent.md +206 -0
- package/agents/ios/swift-reviewer/README.md +68 -0
- package/agents/ios/swift-reviewer/agent.md +231 -0
- package/agents/ios/swift-reviewer/example-input.md +100 -0
- package/agents/ios/swift-reviewer/example-output.md +127 -0
- package/agents/react-native/bridge/agent.md +212 -0
- package/agents/react-native/performance-optimizer/README.md +81 -0
- package/agents/react-native/performance-optimizer/agent.md +284 -0
- package/agents/react-native/performance-optimizer/example-input.md +104 -0
- package/agents/react-native/performance-optimizer/example-output.md +132 -0
- package/agents/unity/shader-generator/README.md +77 -0
- package/agents/unity/shader-generator/agent.md +270 -0
- package/agents/unity/shader-generator/example-input.md +70 -0
- package/agents/unity/shader-generator/example-output.md +160 -0
- package/agents/unreal/blueprint-advisor/README.md +71 -0
- package/agents/unreal/blueprint-advisor/agent.md +311 -0
- package/agents/unreal/blueprint-advisor/example-input.md +73 -0
- package/agents/unreal/blueprint-advisor/example-output.md +217 -0
- package/cli/index.js +1091 -0
- package/docs/agent-guide.md +138 -0
- package/docs/chatgpt-integration.md +219 -0
- package/docs/core-loops.md +143 -0
- package/docs/cursor-integration.md +143 -0
- package/docs/getting-started.md +132 -0
- package/docs/index.html +711 -0
- package/docs/installation.md +271 -0
- package/docs/mobile-harness.md +160 -0
- package/docs/mobile-mcp.md +172 -0
- package/docs/mobile-memory.md +349 -0
- package/docs/release-process.md +85 -0
- package/docs/releases/release-v1.0.0.md +165 -0
- package/docs/roadmap/github-issues.md +239 -0
- package/docs/skill-categories.md +125 -0
- package/docs/vscode-copilot-integration.md +104 -0
- package/docs/wiki.md +615 -0
- package/install.sh +507 -0
- package/mobile-karpathy.md +79 -0
- package/package.json +57 -0
- package/skills/android/README.md +31 -0
- package/skills/android/android-tdd.md +89 -0
- package/skills/android/anr-investigation.md +251 -0
- package/skills/android/code-review.md +98 -0
- package/skills/android/compose-migration.md +48 -0
- package/skills/android/compose-review.md +78 -0
- package/skills/android/kotlin-modernize.md +51 -0
- package/skills/android/memory-leak-investigation.md +256 -0
- package/skills/android/proguard-rules.md +48 -0
- package/skills/cross-platform/accessibility-audit.md +63 -0
- package/skills/cross-platform/api-versioning.md +59 -0
- package/skills/cross-platform/clean-code-audit.md +239 -0
- package/skills/cross-platform/crash-triage.md +53 -0
- package/skills/cross-platform/deeplink-debug.md +57 -0
- package/skills/cross-platform/feature-slice.md +48 -0
- package/skills/cross-platform/grill-mobile.md +64 -0
- package/skills/cross-platform/mobile-mcp-qa.md +193 -0
- package/skills/cross-platform/mobile-memory-graph.md +179 -0
- package/skills/cross-platform/mobile-memory-save.md +210 -0
- package/skills/cross-platform/mobile-memory-search.md +154 -0
- package/skills/cross-platform/perf-audit.md +61 -0
- package/skills/cross-platform/prd-verification.md +324 -0
- package/skills/cross-platform/release-prep.md +64 -0
- package/skills/cross-platform/security-audit.md +281 -0
- package/skills/cross-platform/store-listing.md +75 -0
- package/skills/flutter/README.md +32 -0
- package/skills/flutter/dart-modernize.md +47 -0
- package/skills/flutter/flutter-review.md +59 -0
- package/skills/flutter/flutter-tdd.md +73 -0
- package/skills/flutter/widget-extract.md +41 -0
- package/skills/flutter/widget-gen.md +131 -0
- package/skills/gaming/blueprint-to-cpp.md +51 -0
- package/skills/gaming/game-perf.md +54 -0
- package/skills/gaming/shader-gen.md +44 -0
- package/skills/gaming/unity-tdd.md +85 -0
- package/skills/ios/README.md +45 -0
- package/skills/ios/data-persistence.md +151 -0
- package/skills/ios/ios-tdd.md +90 -0
- package/skills/ios/networking.md +139 -0
- package/skills/ios/performance.md +122 -0
- package/skills/ios/swift-concurrency.md +57 -0
- package/skills/ios/swift-review.md +124 -0
- package/skills/ios/swiftui-review.md +49 -0
- package/skills/ios/swiftui-state.md +122 -0
- package/skills/ios/unit-testing.md +144 -0
- package/skills/ios/xcode-warnings.md +37 -0
- package/skills/react-native/README.md +42 -0
- package/skills/react-native/bridge-audit.md +104 -0
- package/skills/react-native/expo-optimize.md +55 -0
- package/skills/react-native/new-arch-migrate.md +53 -0
- package/skills/react-native/performance.md +124 -0
- package/skills/react-native/rn-review.md +53 -0
- package/skills/react-native/rn-tdd.md +76 -0
- package/skills/shared/README.md +46 -0
- package/skills/shared/accessibility-audit.md +132 -0
- package/skills/shared/crash-analysis.md +110 -0
- package/skills/shared/security-scan.md +149 -0
- package/skills/unity/README.md +31 -0
- package/skills/unity/shader-review.md +110 -0
- package/templates/agent-template.md +88 -0
- package/templates/prompt-template.md +50 -0
- package/templates/skill-template.md +64 -0
- package/templates/workflow-template.md +48 -0
- package/workflows/app-launch.md +66 -0
- package/workflows/appforge-workflow.md +232 -0
- package/workflows/ci-setup.md +77 -0
- package/workflows/crash-to-fix.md +71 -0
- package/workflows/feature-ship.md +61 -0
- package/workflows/game-level.md +72 -0
- package/workflows/mobile-harness.md +319 -0
- package/workflows/mobile-mcp-qa.md +228 -0
- package/workflows/mobile-memory-workflow.md +243 -0
- package/workflows/new-project-setup.md +93 -0
- package/workflows/new-screen.md +68 -0
- package/workflows/perf-sprint.md +78 -0
- package/workflows/release-cycle.md +71 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Example Output — Android Crash Analyzer
|
|
2
|
+
|
|
3
|
+
This is the agent's full response to the crash log in [`example-input.md`](example-input.md).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Crash Summary
|
|
8
|
+
|
|
9
|
+
`IllegalStateException: Fragment ProfileFragment not attached to a context` thrown at `ProfileFragment.kt:124` inside a LiveData observer callback. The crash occurs when a photo upload completes (or fails) after the user has already navigated away from the Profile screen, causing the `viewModelScope` coroutine to post to `_uploadState`, which triggers the still-active LiveData observer, which calls `requireContext()` on a detached Fragment. **HIGH** severity — intermittent, proportional to upload latency and user navigation speed.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Root Cause
|
|
14
|
+
|
|
15
|
+
There are two compounding bugs that together produce this crash:
|
|
16
|
+
|
|
17
|
+
**Bug 1 — Observer registered with wrong LifecycleOwner (`this` vs `viewLifecycleOwner`):**
|
|
18
|
+
```kotlin
|
|
19
|
+
viewModel.uploadState.observe(this) { state -> ... }
|
|
20
|
+
// ^^^^
|
|
21
|
+
// 'this' = the Fragment's lifecycle, which starts at onCreate and ends at onDestroy.
|
|
22
|
+
// The Fragment's VIEW lifecycle (onCreateView → onDestroyView) is shorter.
|
|
23
|
+
// When the user navigates back, the view is destroyed but the Fragment object
|
|
24
|
+
// may still exist in the back stack. The observer remains active.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Bug 2 — `requireContext()` called inside that active observer:**
|
|
28
|
+
```kotlin
|
|
29
|
+
private fun showUploadSuccess(photoUrl: String) {
|
|
30
|
+
val ctx = requireContext() // throws if Fragment is not attached to an Activity
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
When the upload finishes on the `viewModelScope` coroutine (which survives navigation), it posts `UploadState.Success` to `_uploadState`. LiveData dispatches this on the Main thread. The observer fires — because it was registered with `this` (Fragment), not `viewLifecycleOwner` (view). Inside the callback, `requireContext()` throws because the Fragment has been detached from its Activity during back-navigation.
|
|
34
|
+
|
|
35
|
+
**Secondary bug — `Context` passed into `ViewModel`:**
|
|
36
|
+
```kotlin
|
|
37
|
+
fun uploadPhoto(context: Context, bitmap: Bitmap) { ... }
|
|
38
|
+
```
|
|
39
|
+
The ViewModel stores or uses a `Context` from the Fragment. If the repository retains this reference (e.g., for file access), it creates a memory leak tying the Fragment to the ViewModel.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Why This Happens
|
|
44
|
+
|
|
45
|
+
Think of a Fragment as having two lifecycles: the Fragment itself (like the employee) and its View (like the employee's desk). When you use `observe(this)`, you're saying "keep watching until the employee leaves the company." When you use `observe(viewLifecycleOwner)`, you're saying "keep watching until the employee clears their desk."
|
|
46
|
+
|
|
47
|
+
When the user navigates back, the desk is cleared (`onDestroyView`) but the employee is still around (the Fragment stays in the back stack). Your upload finishes, and it tries to show a Snackbar — but the desk is gone. `requireContext()` is like asking "where's my desk?" when there is no desk anymore.
|
|
48
|
+
|
|
49
|
+
The fix is always to watch with `viewLifecycleOwner`, so the observer self-cancels the moment the view is destroyed — no desk, no watch.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Risk Level
|
|
54
|
+
|
|
55
|
+
**HIGH**
|
|
56
|
+
Reproducibility: **Intermittent** — triggered when:
|
|
57
|
+
- Upload takes longer than the user's back-navigation (more likely on slow networks)
|
|
58
|
+
- Device has background CPU throttling (common on budget devices)
|
|
59
|
+
|
|
60
|
+
Affected users: proportional to upload success rate on slow connections × navigation-back rate. In a social app with profile photo uploads, expect this crash in the top 10 Crashlytics issues within weeks of shipping.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Recommended Fix
|
|
65
|
+
|
|
66
|
+
**Step 1 — Change observer to use `viewLifecycleOwner`**
|
|
67
|
+
|
|
68
|
+
This is the primary fix. The observer self-cancels when the view is destroyed, preventing the callback from ever reaching `showUploadSuccess` after navigation.
|
|
69
|
+
|
|
70
|
+
**Step 2 — Remove `requireContext()` from the observer callback**
|
|
71
|
+
|
|
72
|
+
The Fragment always has access to its context through the view binding root (`binding.root.context`) without calling `requireContext()`. This is safer because `binding` is only accessible when the view exists.
|
|
73
|
+
|
|
74
|
+
**Step 3 — Remove `Context` parameter from `ViewModel.uploadPhoto()`**
|
|
75
|
+
|
|
76
|
+
ViewModels must never hold Activity or Fragment context. Use `application.applicationContext` (inject `Application` via Hilt's `@HiltViewModel`) for any context-dependent work in the ViewModel or Repository.
|
|
77
|
+
|
|
78
|
+
**Step 4 — Cancel the upload when the user navigates away (optional but recommended)**
|
|
79
|
+
|
|
80
|
+
If you want uploads to stop when the user leaves, cancel the coroutine Job and expose cancellation via a `cancelUpload()` function.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Updated Code
|
|
85
|
+
|
|
86
|
+
```kotlin
|
|
87
|
+
// ProfileFragment.kt — AFTER
|
|
88
|
+
|
|
89
|
+
class ProfileFragment : Fragment(R.layout.fragment_profile) {
|
|
90
|
+
|
|
91
|
+
private val viewModel: ProfileViewModel by viewModels()
|
|
92
|
+
private var _binding: FragmentProfileBinding? = null
|
|
93
|
+
private val binding get() = _binding!!
|
|
94
|
+
|
|
95
|
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
96
|
+
super.onViewCreated(view, savedInstanceState)
|
|
97
|
+
_binding = FragmentProfileBinding.bind(view)
|
|
98
|
+
|
|
99
|
+
// FIX 1: Use viewLifecycleOwner, not 'this'.
|
|
100
|
+
// The observer now self-cancels when onDestroyView() fires.
|
|
101
|
+
// 'uploadState' is now a StateFlow — collected with repeatOnLifecycle
|
|
102
|
+
// to guarantee cancellation and avoid the race window.
|
|
103
|
+
viewLifecycleOwner.lifecycleScope.launch {
|
|
104
|
+
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
105
|
+
viewModel.uploadState.collect { state ->
|
|
106
|
+
when (state) {
|
|
107
|
+
is UploadState.Success -> showUploadSuccess(state.photoUrl)
|
|
108
|
+
is UploadState.Error -> showError(state.message)
|
|
109
|
+
UploadState.Loading -> binding.uploadProgress.isVisible = true
|
|
110
|
+
UploadState.Idle -> binding.uploadProgress.isVisible = false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
binding.uploadButton.setOnClickListener {
|
|
117
|
+
// FIX 3: No longer passes Context to the ViewModel.
|
|
118
|
+
viewModel.uploadPhoto(getSelectedBitmap())
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private fun showUploadSuccess(photoUrl: String) {
|
|
123
|
+
// FIX 2: binding.root.context is always valid here because
|
|
124
|
+
// repeatOnLifecycle guarantees this block only runs while STARTED.
|
|
125
|
+
// No requireContext() needed.
|
|
126
|
+
binding.uploadProgress.isVisible = false
|
|
127
|
+
Snackbar.make(binding.root, "Photo updated!", Snackbar.LENGTH_SHORT).show()
|
|
128
|
+
Glide.with(binding.root.context).load(photoUrl).into(binding.avatarImage)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
override fun onDestroyView() {
|
|
132
|
+
super.onDestroyView()
|
|
133
|
+
_binding = null
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ProfileViewModel.kt — AFTER
|
|
138
|
+
|
|
139
|
+
@HiltViewModel
|
|
140
|
+
class ProfileViewModel @Inject constructor(
|
|
141
|
+
// FIX 3: Inject Application for context-dependent work.
|
|
142
|
+
// Application context does NOT leak — it lives as long as the process.
|
|
143
|
+
private val application: Application,
|
|
144
|
+
private val photoRepository: PhotoRepository
|
|
145
|
+
) : ViewModel() {
|
|
146
|
+
|
|
147
|
+
// Migrated from MutableLiveData to MutableStateFlow.
|
|
148
|
+
// StateFlow + collectAsStateWithLifecycle/repeatOnLifecycle is the modern pattern.
|
|
149
|
+
private val _uploadState = MutableStateFlow<UploadState>(UploadState.Idle)
|
|
150
|
+
val uploadState: StateFlow<UploadState> = _uploadState.asStateFlow()
|
|
151
|
+
|
|
152
|
+
private var uploadJob: Job? = null
|
|
153
|
+
|
|
154
|
+
fun uploadPhoto(bitmap: Bitmap) {
|
|
155
|
+
// Cancel any in-flight upload before starting a new one.
|
|
156
|
+
uploadJob?.cancel()
|
|
157
|
+
uploadJob = viewModelScope.launch {
|
|
158
|
+
_uploadState.value = UploadState.Loading
|
|
159
|
+
try {
|
|
160
|
+
// Repository now receives applicationContext — no leak.
|
|
161
|
+
val url = photoRepository.uploadPhoto(application.applicationContext, bitmap)
|
|
162
|
+
_uploadState.value = UploadState.Success(url)
|
|
163
|
+
} catch (e: CancellationException) {
|
|
164
|
+
// User navigated away and the coroutine was cancelled — do not emit Error.
|
|
165
|
+
_uploadState.value = UploadState.Idle
|
|
166
|
+
throw e // Always re-throw CancellationException
|
|
167
|
+
} catch (e: Exception) {
|
|
168
|
+
_uploadState.value = UploadState.Error(e.message ?: "Upload failed")
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fun cancelUpload() {
|
|
174
|
+
uploadJob?.cancel()
|
|
175
|
+
_uploadState.value = UploadState.Idle
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// UploadState.kt — sealed class for type-safe state
|
|
180
|
+
|
|
181
|
+
sealed class UploadState {
|
|
182
|
+
object Idle : UploadState()
|
|
183
|
+
object Loading : UploadState()
|
|
184
|
+
data class Success(val photoUrl: String) : UploadState()
|
|
185
|
+
data class Error(val message: String) : UploadState()
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Edge Cases
|
|
192
|
+
|
|
193
|
+
- **ViewPager2 tabs:** Fragments in a ViewPager2 have their views destroyed and recreated when the user swipes away. Using `observe(this)` accumulates a new observer on every tab-return. With the `viewLifecycleOwner` fix, each view creation registers exactly one observer.
|
|
194
|
+
|
|
195
|
+
- **Bottom navigation with `FragmentManager` backstack:** A Fragment that is added to the back stack retains its Fragment instance. After `onDestroyView()`, any observer registered with `this` continues firing. The fix eliminates this entirely.
|
|
196
|
+
|
|
197
|
+
- **Configuration change mid-upload:** The ViewModel survives rotation, but the Fragment is recreated. Without the fix, both the old and new Fragment's observers fire. With the fix, only the new Fragment's observer is active.
|
|
198
|
+
|
|
199
|
+
- **Deep-link navigation to ProfileFragment:** If a deep link arrives while a previous ProfileFragment is still alive in the back stack, the upload from the back-stack Fragment can still post to the ViewModel's StateFlow — but with `repeatOnLifecycle(STARTED)`, only the foreground Fragment collects.
|
|
200
|
+
|
|
201
|
+
- **Application context for Glide:** `Glide.with(context)` where `context` is an Activity or Fragment that has been destroyed will log a warning and sometimes throw. Always use `Glide.with(applicationContext)` for background or long-lived loads.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Testing Checklist
|
|
206
|
+
|
|
207
|
+
- [ ] Open Profile screen → start a photo upload → immediately press Back → confirm **no crash** in logcat or Crashlytics
|
|
208
|
+
- [ ] Open Profile screen → start a photo upload → rotate the device mid-upload → upload completes → confirm result appears correctly in the new Fragment instance
|
|
209
|
+
- [ ] Open Profile screen → start a photo upload → switch to another tab (bottom nav) → switch back → confirm upload state is restored correctly
|
|
210
|
+
- [ ] Open Profile screen → start a photo upload → throttle network to 3G (Developer Options → Network speed) → press Back → confirm no crash
|
|
211
|
+
- [ ] Run the app with LeakCanary enabled → complete a photo upload → navigate away → confirm **no Fragment or Activity leak** is reported
|
|
212
|
+
- [ ] Enable "Don't keep activities" in Developer Options → start upload → press Home → return to app → confirm no crash and UI state is correct
|
|
213
|
+
- [ ] Verify that calling `uploadPhoto()` twice rapidly does not produce two concurrent uploads (second call cancels the first)
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Prevention Tips
|
|
218
|
+
|
|
219
|
+
- **Rule:** Always use `viewLifecycleOwner` when observing LiveData or collecting Flow inside a Fragment. Add this to your code review checklist.
|
|
220
|
+
- **Lint:** Enable the AndroidX Lint rule `FragmentLiveDataObserve` — it flags `observe(this)` inside Fragments automatically.
|
|
221
|
+
- **Migration:** Replace `MutableLiveData` + `observe()` with `MutableStateFlow` + `repeatOnLifecycle(STARTED)` + `collect {}` in all new Fragments. The structured concurrency model eliminates the observer lifecycle class entirely.
|
|
222
|
+
- **Architecture:** ViewModels must never accept `Activity`, `Fragment`, or `View` as parameters. Use `AndroidViewModel` for Application context, or inject `@ApplicationContext` via Hilt.
|
|
223
|
+
- **CancellationException:** Never catch `CancellationException` generically. Use `catch (e: Exception) { if (e is CancellationException) throw e; ... }` or separate `catch` blocks.
|
|
224
|
+
- **Testing:** Add a `WhenFragmentDetachedTest` to your test suite that verifies ViewModel state changes after `fragmentScenario.recreate()` do not crash the Fragment.
|
|
225
|
+
- **Static analysis:** Add `detekt` or `ktlint` rules banning `GlobalScope` and `requireContext()` inside observer callbacks.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Android ANR Investigation Agent
|
|
2
|
+
|
|
3
|
+
> ANR traces, Play Console clusters, and thread dumps -> root cause, production fix, and verification plan.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
- Reads Play Console ANR clusters, Crashlytics ANR reports, `traces.txt`, bug reports, and `ApplicationExitInfo`
|
|
10
|
+
- Classifies input dispatch, service, foreground service, broadcast receiver, JobService, startup, binder, lock, and deadlock ANRs
|
|
11
|
+
- Finds the main thread state and any thread that owns the dependency blocking it
|
|
12
|
+
- Detects main-thread database, disk, network, startup, rendering, SDK, and synchronization bottlenecks
|
|
13
|
+
- Produces production-ready Kotlin fixes with structured concurrency and lifecycle-safe ownership
|
|
14
|
+
- Defines verification steps using StrictMode, system traces, macrobenchmarks, low-end devices, and Play Console monitoring
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Files
|
|
19
|
+
|
|
20
|
+
| File | Purpose |
|
|
21
|
+
|---|---|
|
|
22
|
+
| [`agent.md`](agent.md) | Input/output contract and system prompt |
|
|
23
|
+
| [`example-input.md`](example-input.md) | Real ANR-style trace and related code |
|
|
24
|
+
| [`example-output.md`](example-output.md) | Full deterministic investigation output |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
Use the agent at agents/android/anr-investigation/agent.md.
|
|
32
|
+
|
|
33
|
+
APP_VERSION: 4.8.0 (480)
|
|
34
|
+
ANDROID_VERSION: 14
|
|
35
|
+
DEVICE: Pixel 6a
|
|
36
|
+
ANR_SOURCE: Play Console
|
|
37
|
+
ANR_REASON:
|
|
38
|
+
Input dispatching timed out
|
|
39
|
+
USER_ACTION:
|
|
40
|
+
Cold launch, then tapped Sign in.
|
|
41
|
+
TRACE:
|
|
42
|
+
<paste full ANR trace or thread dump>
|
|
43
|
+
RELATED_CODE:
|
|
44
|
+
<paste Activity, ViewModel, repository, service, receiver, or database code>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Install only this agent:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx mobile-ai-agents add agent anr-investigation
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Design Principles
|
|
56
|
+
|
|
57
|
+
- The main thread is often the victim, not the cause.
|
|
58
|
+
- A high-confidence ANR report needs the main thread plus lock owner, binder peer, or blocking dependency.
|
|
59
|
+
- Do not wrap unknown code in `Dispatchers.IO` blindly.
|
|
60
|
+
- Keep receiver, service, and job lifecycle callbacks short.
|
|
61
|
+
- Reduce lock scope instead of moving lock contention to another thread.
|
|
62
|
+
- Every fix needs a reproducible timeout test or measurable performance check.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Related
|
|
67
|
+
|
|
68
|
+
- [`skills/android/anr-investigation.md`](../../../skills/android/anr-investigation.md)
|
|
69
|
+
- [`agents/android/android-crash-analyzer`](../android-crash-analyzer/)
|
|
70
|
+
- [`agents/android/code-reviewer`](../code-reviewer/)
|
|
71
|
+
- [`agents/android/axiom`](../axiom/)
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Official References
|
|
76
|
+
|
|
77
|
+
- [Android Developers: ANRs](https://developer.android.com/topic/performance/vitals/anr)
|
|
78
|
+
- [Android Developers: Diagnose performance issues](https://developer.android.com/topic/performance)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# FREEZE — Android ANR Investigation Agent
|
|
2
|
+
|
|
3
|
+
**Platform:** Android (Kotlin / Java / Jetpack / Compose)
|
|
4
|
+
**Personality:** Main-thread forensic analyst. Every freeze has a blocking chain.
|
|
5
|
+
**Category:** Debugging / Performance / Reliability
|
|
6
|
+
**Complexity:** High
|
|
7
|
+
**Tested With:** GPT-5.4
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
Analyzes Android ANR traces, Play Console clusters, Crashlytics ANRs, thread dumps, bug reports, `ApplicationExitInfo`, and related source code to identify the blocking chain, root cause, smallest safe fix, and verification plan.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Input Format
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
APP_VERSION: <version/build>
|
|
21
|
+
ANDROID_VERSION: <version/API or unknown>
|
|
22
|
+
DEVICE: <device/model or unknown>
|
|
23
|
+
ANR_SOURCE: <Play Console | Crashlytics | traces.txt | bugreport | ApplicationExitInfo>
|
|
24
|
+
ANR_REASON:
|
|
25
|
+
<timeout reason, cluster title, or user-perceived ANR reason>
|
|
26
|
+
USER_ACTION:
|
|
27
|
+
<what the user was doing>
|
|
28
|
+
TRACE:
|
|
29
|
+
<full ANR trace, main thread, and related worker/binder/lock owner threads>
|
|
30
|
+
RELATED_CODE:
|
|
31
|
+
<Activity, ViewModel, repository, service, receiver, worker, database, SDK init, or Compose code>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Output Format
|
|
37
|
+
|
|
38
|
+
````text
|
|
39
|
+
ANDROID ANR INVESTIGATION
|
|
40
|
+
=========================
|
|
41
|
+
Result: CONFIRMED | LIKELY | INCONCLUSIVE
|
|
42
|
+
Confidence: HIGH | MEDIUM | LOW
|
|
43
|
+
ANR Type:
|
|
44
|
+
Trigger:
|
|
45
|
+
|
|
46
|
+
EVIDENCE SUMMARY
|
|
47
|
+
----------------
|
|
48
|
+
| Evidence | Observation |
|
|
49
|
+
|---|---|
|
|
50
|
+
|
|
51
|
+
THREAD ANALYSIS
|
|
52
|
+
---------------
|
|
53
|
+
| Thread | State | Waiting On / Work | Significance |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
|
|
56
|
+
ROOT CAUSE
|
|
57
|
+
----------
|
|
58
|
+
<proven causal chain, or what remains unknown>
|
|
59
|
+
|
|
60
|
+
FIX
|
|
61
|
+
---
|
|
62
|
+
<smallest safe fix>
|
|
63
|
+
|
|
64
|
+
UPDATED CODE
|
|
65
|
+
------------
|
|
66
|
+
```kotlin
|
|
67
|
+
<corrected code, or "Not available without related source">
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
VERIFICATION
|
|
71
|
+
------------
|
|
72
|
+
- <step>
|
|
73
|
+
|
|
74
|
+
MISSING EVIDENCE
|
|
75
|
+
----------------
|
|
76
|
+
- <item or "None">
|
|
77
|
+
|
|
78
|
+
NEXT ACTION
|
|
79
|
+
-----------
|
|
80
|
+
<one concrete action>
|
|
81
|
+
````
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## System Prompt
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
You are FREEZE, a senior Android ANR investigator specializing in Kotlin, Java, Android vitals, Play Console ANR clusters, traces.txt, ApplicationExitInfo, thread dumps, startup performance, Room/SQLite, coroutines, binder, services, broadcast receivers, JobService, Compose, locks, and deadlocks.
|
|
89
|
+
|
|
90
|
+
Analyze evidence before giving fixes. Locate the main thread, record its state, then identify the dependency that prevents it from processing input, lifecycle callbacks, broadcasts, services, or jobs. The top app frame can be a victim; find the blocking owner when locks, binder, futures, joins, latches, or monitors appear.
|
|
91
|
+
|
|
92
|
+
Rules:
|
|
93
|
+
1. Classify the ANR as input dispatch timeout, service timeout, foreground service start timeout, broadcast receiver timeout, JobService timeout, main-thread I/O/computation, binder stall, lock contention, deadlock, slow startup, rendering/layout stall, or unknown.
|
|
94
|
+
2. For BLOCKED main threads, identify the monitor/lock and owner thread. For WAITING, identify Future.get, CountDownLatch.await, join, runBlocking, wait, semaphore, mutex, or synchronous callback. For NATIVE, inspect binder, file, database, graphics, or native SDK frames.
|
|
95
|
+
3. Check related code for main-thread Room/SQLite, disk, network, SharedPreferences commit, JSON parsing, bitmap work, crypto, eager DI, ContentProvider/Application startup, synchronous SDK init, long receiver/service/job callbacks, and broad synchronized sections.
|
|
96
|
+
4. Propose the smallest safe production fix. Use structured coroutines and appropriate dispatchers, WorkManager for deferrable persistent work, shorter service/receiver/job callbacks, reduced lock scope, async SDK initialization, or startup deferral as evidence requires.
|
|
97
|
+
5. Do not blindly suggest Dispatchers.IO when the cause is lock contention, binder stall, deadlock, or lifecycle misuse.
|
|
98
|
+
6. Include corrected Kotlin/Java when source is present. Preserve cancellation, lifecycle, and error handling.
|
|
99
|
+
7. Include verification with reproduction, low-end device testing, StrictMode or tracing, Macrobenchmark/startup measurement when relevant, and Play Console ANR cluster monitoring.
|
|
100
|
+
8. If trace evidence is incomplete, mark confidence LOW or MEDIUM and list exactly what is missing.
|
|
101
|
+
|
|
102
|
+
Output MUST follow the exact format specified. Do not add extra sections or omit any section.
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Example
|
|
108
|
+
|
|
109
|
+
See [`example-input.md`](example-input.md) and [`example-output.md`](example-output.md).
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Notes
|
|
114
|
+
|
|
115
|
+
- ANR reports need full thread context; a single main-thread frame is rarely enough.
|
|
116
|
+
- Use the dedicated `/anr-investigation` skill for smaller inline analysis.
|
|
117
|
+
- Pair with PERF for startup traces and with Mobile MCP QA for device reproduction.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Example Input — Android ANR Investigation Agent
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
APP_VERSION: 4.8.0 (480)
|
|
5
|
+
ANDROID_VERSION: Android 14 / API 34
|
|
6
|
+
DEVICE: Pixel 6a
|
|
7
|
+
ANR_SOURCE: Play Console
|
|
8
|
+
ANR_REASON:
|
|
9
|
+
Input dispatching timed out. The application was too busy to process user input.
|
|
10
|
+
USER_ACTION:
|
|
11
|
+
Cold launch, then tapped Sign in while session refresh was running.
|
|
12
|
+
|
|
13
|
+
TRACE:
|
|
14
|
+
"main" prio=5 tid=1 BLOCKED
|
|
15
|
+
at com.sampleshop.session.SessionStore.read(SessionStore.kt:42)
|
|
16
|
+
- waiting to lock <0x1234> (a com.sampleshop.session.SessionStore) held by thread 18
|
|
17
|
+
at com.sampleshop.auth.AuthRepository.currentSession(AuthRepository.kt:27)
|
|
18
|
+
at com.sampleshop.ui.MainActivity.onCreate(MainActivity.kt:31)
|
|
19
|
+
at android.app.Activity.performCreate(Activity.java:8595)
|
|
20
|
+
|
|
21
|
+
"DefaultDispatcher-worker-3" prio=5 tid=18 RUNNABLE
|
|
22
|
+
at java.net.SocketInputStream.socketRead0(Native Method)
|
|
23
|
+
at okhttp3.internal.connection.RealCall.execute(RealCall.kt:153)
|
|
24
|
+
at com.sampleshop.session.SessionApi.refreshTokenBlocking(SessionApi.kt:21)
|
|
25
|
+
at com.sampleshop.session.SessionStore.refresh(SessionStore.kt:68)
|
|
26
|
+
- locked <0x1234> (a com.sampleshop.session.SessionStore)
|
|
27
|
+
at com.sampleshop.session.SessionRepository.refresh(SessionRepository.kt:51)
|
|
28
|
+
|
|
29
|
+
RELATED_CODE:
|
|
30
|
+
class SessionStore(
|
|
31
|
+
private val api: SessionApi
|
|
32
|
+
) {
|
|
33
|
+
private var cachedSession: Session? = null
|
|
34
|
+
|
|
35
|
+
@Synchronized
|
|
36
|
+
fun read(): Session? = cachedSession
|
|
37
|
+
|
|
38
|
+
@Synchronized
|
|
39
|
+
fun refresh(): Session {
|
|
40
|
+
val response = api.refreshTokenBlocking()
|
|
41
|
+
val session = response.toSession()
|
|
42
|
+
cachedSession = session
|
|
43
|
+
return session
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class AuthRepository(
|
|
48
|
+
private val sessionStore: SessionStore
|
|
49
|
+
) {
|
|
50
|
+
fun currentSession(): Session? = sessionStore.read()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class MainActivity : ComponentActivity() {
|
|
54
|
+
@Inject lateinit var authRepository: AuthRepository
|
|
55
|
+
|
|
56
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
57
|
+
super.onCreate(savedInstanceState)
|
|
58
|
+
val session = authRepository.currentSession()
|
|
59
|
+
setContent { App(session = session) }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Example Output — Android ANR Investigation Agent
|
|
2
|
+
|
|
3
|
+
````text
|
|
4
|
+
ANDROID ANR INVESTIGATION
|
|
5
|
+
=========================
|
|
6
|
+
Result: CONFIRMED
|
|
7
|
+
Confidence: HIGH
|
|
8
|
+
ANR Type: Lock contention causing input dispatch timeout
|
|
9
|
+
Trigger: Cold launch while session refresh holds the SessionStore monitor
|
|
10
|
+
|
|
11
|
+
EVIDENCE SUMMARY
|
|
12
|
+
----------------
|
|
13
|
+
| Evidence | Observation |
|
|
14
|
+
|---|---|
|
|
15
|
+
| ANR reason | Input dispatching timed out |
|
|
16
|
+
| Main thread | BLOCKED in `SessionStore.read(SessionStore.kt:42)` |
|
|
17
|
+
| Lock | Main waits for monitor `0x1234` |
|
|
18
|
+
| Owner thread | `DefaultDispatcher-worker-3` owns `0x1234` |
|
|
19
|
+
| Owner work | Blocking OkHttp refresh inside `SessionStore.refresh()` |
|
|
20
|
+
|
|
21
|
+
THREAD ANALYSIS
|
|
22
|
+
---------------
|
|
23
|
+
| Thread | State | Waiting On / Work | Significance |
|
|
24
|
+
|---|---|---|---|
|
|
25
|
+
| main | BLOCKED | Waiting for `SessionStore` monitor `0x1234` | Cannot finish launch or process input |
|
|
26
|
+
| DefaultDispatcher-worker-3 | RUNNABLE | Blocking network call while holding `SessionStore` monitor | Prevents main from reading cached session |
|
|
27
|
+
|
|
28
|
+
ROOT CAUSE
|
|
29
|
+
----------
|
|
30
|
+
`SessionStore.refresh()` is synchronized and holds the object monitor while performing a blocking network request. During cold launch, `MainActivity.onCreate()` calls `AuthRepository.currentSession()`, which calls the synchronized `SessionStore.read()`. Because the worker thread owns the same monitor until the network request completes, the main thread blocks and cannot process the user's sign-in tap. That produces the input dispatch timeout.
|
|
31
|
+
|
|
32
|
+
FIX
|
|
33
|
+
---
|
|
34
|
+
1. Do not hold the `SessionStore` monitor during network I/O.
|
|
35
|
+
2. Replace method-level `@Synchronized` with a narrow lock only around cached state reads/writes.
|
|
36
|
+
3. Make refresh suspendable and perform the blocking call on `Dispatchers.IO` only for the network operation.
|
|
37
|
+
4. Keep launch rendering independent from refresh completion; read cached state quickly and let the UI observe refresh state asynchronously.
|
|
38
|
+
|
|
39
|
+
UPDATED CODE
|
|
40
|
+
------------
|
|
41
|
+
```kotlin
|
|
42
|
+
class SessionStore(
|
|
43
|
+
private val api: SessionApi,
|
|
44
|
+
) {
|
|
45
|
+
private val lock = Any()
|
|
46
|
+
private var cachedSession: Session? = null
|
|
47
|
+
|
|
48
|
+
fun read(): Session? = synchronized(lock) {
|
|
49
|
+
cachedSession
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
suspend fun refresh(): Session {
|
|
53
|
+
val session = withContext(Dispatchers.IO) {
|
|
54
|
+
api.refreshTokenBlocking().toSession()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
synchronized(lock) {
|
|
58
|
+
cachedSession = session
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return session
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class AuthRepository(
|
|
66
|
+
private val sessionStore: SessionStore,
|
|
67
|
+
) {
|
|
68
|
+
fun currentSession(): Session? = sessionStore.read()
|
|
69
|
+
|
|
70
|
+
suspend fun refreshSession(): Session = sessionStore.refresh()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class MainActivity : ComponentActivity() {
|
|
74
|
+
@Inject lateinit var authRepository: AuthRepository
|
|
75
|
+
|
|
76
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
77
|
+
super.onCreate(savedInstanceState)
|
|
78
|
+
val cachedSession = authRepository.currentSession()
|
|
79
|
+
|
|
80
|
+
setContent {
|
|
81
|
+
App(session = cachedSession)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
VERIFICATION
|
|
88
|
+
------------
|
|
89
|
+
- Add a test fake that delays `refreshTokenBlocking()` by 10 seconds while launching `MainActivity`.
|
|
90
|
+
- Confirm `MainActivity.onCreate()` returns without waiting for refresh.
|
|
91
|
+
- Enable StrictMode and verify no main-thread disk/network violations during startup.
|
|
92
|
+
- Capture a Perfetto trace during cold launch and confirm the main thread is not BLOCKED on `SessionStore`.
|
|
93
|
+
- Run startup on a low-end Android device or emulator with CPU throttling.
|
|
94
|
+
- Monitor the Play Console ANR cluster after staged rollout.
|
|
95
|
+
|
|
96
|
+
MISSING EVIDENCE
|
|
97
|
+
----------------
|
|
98
|
+
- None
|
|
99
|
+
|
|
100
|
+
NEXT ACTION
|
|
101
|
+
-----------
|
|
102
|
+
Replace method-level synchronization in `SessionStore` with narrow cached-state locking, then rerun the delayed-refresh cold-launch reproduction.
|
|
103
|
+
````
|