androjack-mcp 1.3.0
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/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +34 -0
- package/.github/pull_request_template.md +16 -0
- package/CONTRIBUTING.md +27 -0
- package/LICENSE +21 -0
- package/README.md +592 -0
- package/SECURITY.md +26 -0
- package/assets/AndroJack banner.png +0 -0
- package/assets/killer_argument.png +0 -0
- package/build/constants.js +412 -0
- package/build/http-server.js +163 -0
- package/build/http.js +151 -0
- package/build/index.js +553 -0
- package/build/install.js +379 -0
- package/build/logger.js +57 -0
- package/build/tools/api-level.js +170 -0
- package/build/tools/api36-compliance.js +282 -0
- package/build/tools/architecture.js +75 -0
- package/build/tools/build-publish.js +362 -0
- package/build/tools/component.js +90 -0
- package/build/tools/debugger.js +82 -0
- package/build/tools/gradle.js +234 -0
- package/build/tools/kmp.js +348 -0
- package/build/tools/kotlin-patterns.js +500 -0
- package/build/tools/large-screen.js +366 -0
- package/build/tools/m3-expressive.js +447 -0
- package/build/tools/navigation3.js +331 -0
- package/build/tools/ondevice-ai.js +283 -0
- package/build/tools/permissions.js +404 -0
- package/build/tools/play-policy.js +221 -0
- package/build/tools/scalability.js +621 -0
- package/build/tools/search.js +89 -0
- package/build/tools/testing.js +439 -0
- package/build/tools/wear.js +337 -0
- package/build/tools/xr.js +274 -0
- package/config/antigravity_mcp.json +32 -0
- package/config/claude_desktop_config.json +17 -0
- package/config/cursor_mcp.json +21 -0
- package/config/jetbrains_mcp.json +28 -0
- package/config/kiro_mcp.json +40 -0
- package/config/vscode_mcp.json +24 -0
- package/config/windsurf_mcp.json +18 -0
- package/package.json +51 -0
- package/src/constants.ts +436 -0
- package/src/http-server.ts +186 -0
- package/src/http.ts +190 -0
- package/src/index.ts +702 -0
- package/src/install.ts +441 -0
- package/src/logger.ts +67 -0
- package/src/tools/api-level.ts +198 -0
- package/src/tools/api36-compliance.ts +289 -0
- package/src/tools/architecture.ts +94 -0
- package/src/tools/build-publish.ts +379 -0
- package/src/tools/component.ts +106 -0
- package/src/tools/debugger.ts +111 -0
- package/src/tools/gradle.ts +288 -0
- package/src/tools/kmp.ts +352 -0
- package/src/tools/kotlin-patterns.ts +534 -0
- package/src/tools/large-screen.ts +391 -0
- package/src/tools/m3-expressive.ts +473 -0
- package/src/tools/navigation3.ts +338 -0
- package/src/tools/ondevice-ai.ts +287 -0
- package/src/tools/permissions.ts +445 -0
- package/src/tools/play-policy.ts +229 -0
- package/src/tools/scalability.ts +646 -0
- package/src/tools/search.ts +112 -0
- package/src/tools/testing.ts +460 -0
- package/src/tools/wear.ts +343 -0
- package/src/tools/xr.ts +278 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// Tool 14: Navigation 3 Guide
|
|
2
|
+
// Navigation 3 went stable November 2025 — complete architectural rethink from Nav2.
|
|
3
|
+
// This tool exists because AI tools hallucinate Nav2 code for new projects even with
|
|
4
|
+
// internet access (documented Atomic Robot case study, January 2026).
|
|
5
|
+
export async function androidNavigation3Guide(topic) {
|
|
6
|
+
const t = topic.toLowerCase().trim();
|
|
7
|
+
// ── Overview / default ─────────────────────────────────────────────────────
|
|
8
|
+
const overview = `
|
|
9
|
+
# Navigation 3 — Official Reference (Stable, November 2025)
|
|
10
|
+
Source: https://developer.android.com/guide/navigation/navigation-3
|
|
11
|
+
|
|
12
|
+
## ⚠️ CRITICAL FOR AI TOOLS
|
|
13
|
+
Navigation 3 is the current official navigation library as of November 2025.
|
|
14
|
+
Do NOT generate Navigation 2 (NavController/NavHost/NavGraph) for new projects.
|
|
15
|
+
Google's Nav3 migration docs contain explicit "AI Agent:" annotations — this is how
|
|
16
|
+
aware Google is that AI tools hallucinate stale navigation code.
|
|
17
|
+
|
|
18
|
+
## Why Nav3 Is a Breaking Paradigm Shift (Not Just a Version Bump)
|
|
19
|
+
|
|
20
|
+
| Concept | Navigation 2 | Navigation 3 |
|
|
21
|
+
|---------|-------------|-------------|
|
|
22
|
+
| Back stack | Internal library state | Plain Kotlin List<NavEntry<*>> |
|
|
23
|
+
| State management | NavController owns it | You own it (observable, testable) |
|
|
24
|
+
| Navigation trigger | NavController.navigate() | Add/remove from your back stack list |
|
|
25
|
+
| Graph definition | NavHost + composable() destinations | NavDisplay + type-safe NavKey |
|
|
26
|
+
| Adaptive layouts | Separate NavigationSuiteScaffold | Built-in via Scenes API |
|
|
27
|
+
| State persistence | rememberSaveable | rememberNavBackStack (custom serialization) |
|
|
28
|
+
| Testing | Hard — internal state | Easy — back stack is a plain list |
|
|
29
|
+
|
|
30
|
+
## Core Setup
|
|
31
|
+
|
|
32
|
+
\`\`\`kotlin
|
|
33
|
+
// libs.versions.toml
|
|
34
|
+
[versions]
|
|
35
|
+
navigation3 = "1.0.0" // Stable November 2025
|
|
36
|
+
|
|
37
|
+
[libraries]
|
|
38
|
+
navigation3-compose = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "navigation3" }
|
|
39
|
+
navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime", version.ref = "navigation3" }
|
|
40
|
+
\`\`\`
|
|
41
|
+
|
|
42
|
+
\`\`\`kotlin
|
|
43
|
+
// build.gradle.kts
|
|
44
|
+
implementation(libs.navigation3.compose)
|
|
45
|
+
implementation(libs.navigation3.runtime)
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
## NavKey — Type-Safe Destination Keys
|
|
49
|
+
|
|
50
|
+
\`\`\`kotlin
|
|
51
|
+
// Each destination is a serializable key — NOT a route string
|
|
52
|
+
@Serializable
|
|
53
|
+
data object HomeKey // Simple screen
|
|
54
|
+
|
|
55
|
+
@Serializable
|
|
56
|
+
data class ProfileKey(val userId: String) // Screen with args — type-safe, no stringly-typed routes
|
|
57
|
+
|
|
58
|
+
@Serializable
|
|
59
|
+
data class DetailKey(val itemId: Int, val title: String)
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
62
|
+
## rememberNavBackStack — Your Back Stack
|
|
63
|
+
|
|
64
|
+
\`\`\`kotlin
|
|
65
|
+
// The back stack is a plain Kotlin list you control
|
|
66
|
+
// NOT NavController.navigate() — you add/remove entries directly
|
|
67
|
+
val backStack = rememberNavBackStack(HomeKey) // Start with HomeKey
|
|
68
|
+
|
|
69
|
+
// Navigate forward
|
|
70
|
+
backStack.add(ProfileKey(userId = "abc123"))
|
|
71
|
+
|
|
72
|
+
// Go back
|
|
73
|
+
backStack.removeLastOrNull()
|
|
74
|
+
|
|
75
|
+
// Go back to specific destination (like popUpTo)
|
|
76
|
+
backStack.removeAll { it.key is ProfileKey }
|
|
77
|
+
|
|
78
|
+
// Replace current
|
|
79
|
+
backStack[backStack.lastIndex] = DetailKey(itemId = 1, title = "Item")
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
## NavDisplay — The Renderer
|
|
83
|
+
|
|
84
|
+
\`\`\`kotlin
|
|
85
|
+
@Composable
|
|
86
|
+
fun AppNavigation() {
|
|
87
|
+
val backStack = rememberNavBackStack(HomeKey)
|
|
88
|
+
|
|
89
|
+
NavDisplay(
|
|
90
|
+
backStack = backStack,
|
|
91
|
+
entryProvider = entryProvider {
|
|
92
|
+
entry<HomeKey> { HomeScreen(onNavigateToProfile = { id -> backStack.add(ProfileKey(id)) }) }
|
|
93
|
+
entry<ProfileKey> { key -> ProfileScreen(userId = key.userId) }
|
|
94
|
+
entry<DetailKey> { key -> DetailScreen(itemId = key.itemId, title = key.title) }
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
\`\`\`
|
|
99
|
+
|
|
100
|
+
## State Persistence — rememberSerializable (NOT rememberSaveable)
|
|
101
|
+
|
|
102
|
+
\`\`\`kotlin
|
|
103
|
+
// ❌ WRONG — rememberSaveable does NOT work for NavBackStack
|
|
104
|
+
val backStack = rememberSaveable { mutableStateListOf(HomeKey) }
|
|
105
|
+
|
|
106
|
+
// ✅ CORRECT — rememberNavBackStack uses rememberSerializable internally
|
|
107
|
+
// All NavKey types MUST be @Serializable
|
|
108
|
+
val backStack = rememberNavBackStack(HomeKey)
|
|
109
|
+
\`\`\`
|
|
110
|
+
|
|
111
|
+
Source: https://developer.android.com/guide/navigation/navigation-3#state-persistence
|
|
112
|
+
`;
|
|
113
|
+
// ── Scenes API ─────────────────────────────────────────────────────────────
|
|
114
|
+
const scenes = `
|
|
115
|
+
# Navigation 3 — Scenes API (Adaptive Multi-Pane)
|
|
116
|
+
Source: https://developer.android.com/guide/navigation/navigation-3/adaptive
|
|
117
|
+
|
|
118
|
+
## What Scenes Does
|
|
119
|
+
|
|
120
|
+
The Scenes API replaces the need to combine NavigationSuiteScaffold + separate adaptive
|
|
121
|
+
logic. Nav3 Scenes makes the back stack itself adaptive — the same back stack entries
|
|
122
|
+
automatically render as single-pane on phones or multi-pane on tablets/foldables.
|
|
123
|
+
|
|
124
|
+
\`\`\`kotlin
|
|
125
|
+
// Additional dependency for Scenes
|
|
126
|
+
implementation("androidx.navigation3:navigation3-adaptive:1.0.0")
|
|
127
|
+
\`\`\`
|
|
128
|
+
|
|
129
|
+
## List-Detail with Scenes
|
|
130
|
+
|
|
131
|
+
\`\`\`kotlin
|
|
132
|
+
@Serializable data object InboxKey
|
|
133
|
+
@Serializable data class EmailKey(val emailId: String)
|
|
134
|
+
|
|
135
|
+
@Composable
|
|
136
|
+
fun AdaptiveMailApp() {
|
|
137
|
+
val backStack = rememberNavBackStack(InboxKey)
|
|
138
|
+
val adaptiveInfo = currentWindowAdaptiveInfo()
|
|
139
|
+
|
|
140
|
+
NavDisplay(
|
|
141
|
+
backStack = backStack,
|
|
142
|
+
// Scenes tells NavDisplay how many entries to show simultaneously
|
|
143
|
+
sceneStrategy = rememberListDetailSceneStrategy(),
|
|
144
|
+
entryProvider = entryProvider {
|
|
145
|
+
entry<InboxKey> {
|
|
146
|
+
InboxScreen(onEmailClick = { id -> backStack.add(EmailKey(id)) })
|
|
147
|
+
}
|
|
148
|
+
entry<EmailKey> { key ->
|
|
149
|
+
// On phone: full screen. On tablet: side-by-side with Inbox.
|
|
150
|
+
// Zero conditional logic here — Scenes handles it.
|
|
151
|
+
EmailDetailScreen(emailId = key.emailId)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
\`\`\`
|
|
157
|
+
|
|
158
|
+
## Custom Scene Strategy
|
|
159
|
+
|
|
160
|
+
\`\`\`kotlin
|
|
161
|
+
// Show up to 2 entries side-by-side on Expanded width
|
|
162
|
+
val sceneStrategy = remember {
|
|
163
|
+
TwoPaneSceneStrategy<Any>(
|
|
164
|
+
showTwoPanes = { adaptiveInfo ->
|
|
165
|
+
adaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
\`\`\`
|
|
170
|
+
|
|
171
|
+
Source: https://developer.android.com/guide/navigation/navigation-3/adaptive
|
|
172
|
+
`;
|
|
173
|
+
// ── Migration from Nav2 ────────────────────────────────────────────────────
|
|
174
|
+
const migration = `
|
|
175
|
+
# Navigation 3 — Migrating from Navigation 2
|
|
176
|
+
Source: https://developer.android.com/guide/navigation/navigation-3/migrate
|
|
177
|
+
|
|
178
|
+
## AI Agent: Read This Section Carefully
|
|
179
|
+
Google embedded "AI Agent:" annotations in the official Nav3 migration guide specifically
|
|
180
|
+
because AI tools were hallucinating outdated Nav3 code. This tool surfaces the key points.
|
|
181
|
+
|
|
182
|
+
## API Mapping
|
|
183
|
+
|
|
184
|
+
| Navigation 2 | Navigation 3 Equivalent |
|
|
185
|
+
|-------------|------------------------|
|
|
186
|
+
| NavController | rememberNavBackStack (your list) |
|
|
187
|
+
| NavHost | NavDisplay |
|
|
188
|
+
| composable("route") { } | entry<MyKey> { } |
|
|
189
|
+
| navController.navigate("route") | backStack.add(MyKey) |
|
|
190
|
+
| navController.popBackStack() | backStack.removeLastOrNull() |
|
|
191
|
+
| navController.navigate("route") { popUpTo("other") } | backStack.removeAll { it.key is OtherKey }; backStack.add(MyKey) |
|
|
192
|
+
| arguments bundle | NavKey constructor params (type-safe) |
|
|
193
|
+
| NavDeepLink | Handled separately, not via NavKey |
|
|
194
|
+
| NavGraph nesting | Just add entries — no graph nesting concept |
|
|
195
|
+
|
|
196
|
+
## Migration Steps
|
|
197
|
+
|
|
198
|
+
1. **Replace string routes with @Serializable data class/object NavKeys**
|
|
199
|
+
\`\`\`kotlin
|
|
200
|
+
// Before (Nav2)
|
|
201
|
+
composable("profile/{userId}") { backStackEntry ->
|
|
202
|
+
val userId = backStackEntry.arguments?.getString("userId")
|
|
203
|
+
ProfileScreen(userId)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// After (Nav3)
|
|
207
|
+
@Serializable data class ProfileKey(val userId: String)
|
|
208
|
+
entry<ProfileKey> { key -> ProfileScreen(key.userId) }
|
|
209
|
+
\`\`\`
|
|
210
|
+
|
|
211
|
+
2. **Replace NavController with your back stack**
|
|
212
|
+
\`\`\`kotlin
|
|
213
|
+
// Before
|
|
214
|
+
navController.navigate("profile/abc123")
|
|
215
|
+
|
|
216
|
+
// After
|
|
217
|
+
backStack.add(ProfileKey(userId = "abc123"))
|
|
218
|
+
\`\`\`
|
|
219
|
+
|
|
220
|
+
3. **Remove NavHost, replace with NavDisplay**
|
|
221
|
+
\`\`\`kotlin
|
|
222
|
+
// Before
|
|
223
|
+
NavHost(navController, startDestination = "home") { ... }
|
|
224
|
+
|
|
225
|
+
// After
|
|
226
|
+
NavDisplay(backStack = backStack, entryProvider = entryProvider { ... })
|
|
227
|
+
\`\`\`
|
|
228
|
+
|
|
229
|
+
4. **Update ViewModel navigation** — pass backStack as a parameter or use a shared state holder
|
|
230
|
+
\`\`\`kotlin
|
|
231
|
+
// ViewModel cannot hold NavBackStack directly (lifecycle reasons)
|
|
232
|
+
// Pass navigation callbacks instead
|
|
233
|
+
@Composable
|
|
234
|
+
fun HomeScreen(onNavigateToProfile: (String) -> Unit) { ... }
|
|
235
|
+
|
|
236
|
+
// In NavDisplay entry:
|
|
237
|
+
entry<HomeKey> {
|
|
238
|
+
HomeScreen(onNavigateToProfile = { id -> backStack.add(ProfileKey(id)) })
|
|
239
|
+
}
|
|
240
|
+
\`\`\`
|
|
241
|
+
|
|
242
|
+
## What Does NOT Change in Nav3
|
|
243
|
+
|
|
244
|
+
- Bottom navigation / NavigationRail / NavigationDrawer — still use NavigationSuiteScaffold
|
|
245
|
+
(or use the Scenes API which does it automatically for the content area)
|
|
246
|
+
- Deep links — handled at the Activity level, then push the appropriate NavKey onto the stack
|
|
247
|
+
- ViewModel scoping — still use viewModel() inside entry { } blocks, scoped to that entry
|
|
248
|
+
|
|
249
|
+
Source: https://developer.android.com/guide/navigation/navigation-3/migrate
|
|
250
|
+
`;
|
|
251
|
+
// ── Testing ────────────────────────────────────────────────────────────────
|
|
252
|
+
const testing = `
|
|
253
|
+
# Navigation 3 — Testing
|
|
254
|
+
Source: https://developer.android.com/guide/navigation/navigation-3/testing
|
|
255
|
+
|
|
256
|
+
## Why Nav3 Testing Is Easier Than Nav2
|
|
257
|
+
|
|
258
|
+
The back stack is a plain Kotlin list. You don't need a NavController mock.
|
|
259
|
+
You don't need NavHostFragment. You just observe the list.
|
|
260
|
+
|
|
261
|
+
\`\`\`kotlin
|
|
262
|
+
@Test
|
|
263
|
+
fun navigatingToProfile_addsProfileKeyToBackStack() {
|
|
264
|
+
// Arrange
|
|
265
|
+
val backStack = mutableStateListOf<Any>(HomeKey)
|
|
266
|
+
|
|
267
|
+
// Act
|
|
268
|
+
backStack.add(ProfileKey(userId = "user1"))
|
|
269
|
+
|
|
270
|
+
// Assert — direct list assertion, no NavController involved
|
|
271
|
+
assertThat(backStack.last()).isEqualTo(ProfileKey(userId = "user1"))
|
|
272
|
+
assertThat(backStack.size).isEqualTo(2)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@Test
|
|
276
|
+
fun backPress_removesLastEntry() {
|
|
277
|
+
val backStack = mutableStateListOf<Any>(HomeKey, ProfileKey("u1"))
|
|
278
|
+
backStack.removeLastOrNull()
|
|
279
|
+
assertThat(backStack.last()).isEqualTo(HomeKey)
|
|
280
|
+
}
|
|
281
|
+
\`\`\`
|
|
282
|
+
|
|
283
|
+
## Compose UI Test with Nav3
|
|
284
|
+
|
|
285
|
+
\`\`\`kotlin
|
|
286
|
+
@get:Rule val composeTestRule = createComposeRule()
|
|
287
|
+
|
|
288
|
+
@Test
|
|
289
|
+
fun clickingProfile_showsProfileScreen() {
|
|
290
|
+
val backStack = mutableStateListOf<Any>(HomeKey)
|
|
291
|
+
|
|
292
|
+
composeTestRule.setContent {
|
|
293
|
+
NavDisplay(
|
|
294
|
+
backStack = backStack,
|
|
295
|
+
entryProvider = entryProvider {
|
|
296
|
+
entry<HomeKey> { HomeScreen(onNavigateToProfile = { backStack.add(ProfileKey(it)) }) }
|
|
297
|
+
entry<ProfileKey> { key -> ProfileScreen(userId = key.userId) }
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
composeTestRule.onNodeWithTag("profileButton").performClick()
|
|
303
|
+
composeTestRule.onNodeWithText("Profile").assertIsDisplayed()
|
|
304
|
+
assertThat(backStack.last()).isInstanceOf(ProfileKey::class.java)
|
|
305
|
+
}
|
|
306
|
+
\`\`\`
|
|
307
|
+
|
|
308
|
+
Source: https://developer.android.com/guide/navigation/navigation-3/testing
|
|
309
|
+
`;
|
|
310
|
+
// ── Route dispatch ─────────────────────────────────────────────────────────
|
|
311
|
+
if (t.includes("scene") || t.includes("adaptive") || t.includes("pane") || t.includes("tablet")) {
|
|
312
|
+
return scenes;
|
|
313
|
+
}
|
|
314
|
+
if (t.includes("migrat") || t.includes("nav2") || t.includes("upgrade")) {
|
|
315
|
+
return migration;
|
|
316
|
+
}
|
|
317
|
+
if (t.includes("test")) {
|
|
318
|
+
return testing;
|
|
319
|
+
}
|
|
320
|
+
if (t.includes("key") || t.includes("backstack") || t.includes("back stack") || t.includes("display")) {
|
|
321
|
+
return overview;
|
|
322
|
+
}
|
|
323
|
+
// Default: overview + scenes summary
|
|
324
|
+
return overview + "\n\n---\n\n" + scenes.split("\n## What Scenes Does")[0] +
|
|
325
|
+
"\n\n## Quick Scenes Example\n" +
|
|
326
|
+
"For adaptive list-detail navigation, use `rememberListDetailSceneStrategy()` — " +
|
|
327
|
+
"the same back stack renders as single-pane on phones and two-pane on tablets.\n" +
|
|
328
|
+
"Query 'scenes' for the full example.\n\n" +
|
|
329
|
+
"**Other topics:** 'migration' (from Nav2), 'testing', 'scenes' (adaptive multi-pane)\n\n" +
|
|
330
|
+
"Source: https://developer.android.com/guide/navigation/navigation-3";
|
|
331
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// Tool 17: On-Device AI / AICore Guide
|
|
2
|
+
// Android 16 shipped AICore and ML Kit Gen AI API — local LLMs on Android, no server round-trip.
|
|
3
|
+
// Used by Gmail (Smart Reply), Google Photos (object detection), Pixel Screenshots (semantic search).
|
|
4
|
+
// No MCP server covers this. AI tools default to cloud API calls when on-device is now the answer.
|
|
5
|
+
export async function androidOnDeviceAiGuide(topic) {
|
|
6
|
+
const t = topic.toLowerCase().trim();
|
|
7
|
+
const overview = `
|
|
8
|
+
# On-Device AI — Android AICore & ML Kit Gen AI (2025)
|
|
9
|
+
Source: https://developer.android.com/ai/aicore
|
|
10
|
+
Source: https://developers.google.com/ml-kit/genai
|
|
11
|
+
|
|
12
|
+
## What On-Device AI Means in 2026
|
|
13
|
+
|
|
14
|
+
Android 16 added AICore — a system-level service that manages on-device LLMs.
|
|
15
|
+
ML Kit Gen AI API lets apps access these models through a standard interface.
|
|
16
|
+
|
|
17
|
+
Key benefits vs cloud:
|
|
18
|
+
- **Zero latency** — no network round-trip
|
|
19
|
+
- **Privacy** — data never leaves the device
|
|
20
|
+
- **Offline** — works without internet
|
|
21
|
+
- **Cost** — no API billing
|
|
22
|
+
|
|
23
|
+
## Which Google Apps Use It
|
|
24
|
+
|
|
25
|
+
- **Gmail** — Smart Reply and Smart Compose on Pixel devices (on-device)
|
|
26
|
+
- **Google Photos** — object detection, face grouping, on-device search
|
|
27
|
+
- **Google Docs** — local summarization on Pixel
|
|
28
|
+
- **Pixel Screenshots** — semantic search across screenshot history
|
|
29
|
+
|
|
30
|
+
## Architecture Pattern — Repository Interface for Swappable Backends
|
|
31
|
+
|
|
32
|
+
The official Google pattern: wrap ML models behind a repository interface so the
|
|
33
|
+
implementation can switch between on-device (AICore) and cloud (Vertex AI) without
|
|
34
|
+
touching the UI layer. This is the same MVVM/repository pattern you already know.
|
|
35
|
+
|
|
36
|
+
\`\`\`kotlin
|
|
37
|
+
// Domain layer — platform-agnostic interface
|
|
38
|
+
interface AiTextRepository {
|
|
39
|
+
suspend fun summarize(text: String): Result<String>
|
|
40
|
+
suspend fun generateReply(context: String, options: List<String>): Result<String>
|
|
41
|
+
fun isAvailable(): Flow<Boolean>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Data layer — on-device implementation
|
|
45
|
+
class OnDeviceAiRepository(
|
|
46
|
+
private val generativeModel: GenerativeModel // ML Kit Gen AI
|
|
47
|
+
) : AiTextRepository {
|
|
48
|
+
override suspend fun summarize(text: String): Result<String> = runCatching {
|
|
49
|
+
val response = generativeModel.generateContent("Summarize: $text")
|
|
50
|
+
response.text ?: throw IllegalStateException("No response from on-device model")
|
|
51
|
+
}
|
|
52
|
+
override fun isAvailable(): Flow<Boolean> = flow {
|
|
53
|
+
emit(GenerativeModel.isAvailable())
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Data layer — cloud fallback implementation
|
|
58
|
+
class CloudAiRepository(
|
|
59
|
+
private val vertexAi: FirebaseVertexAI
|
|
60
|
+
) : AiTextRepository {
|
|
61
|
+
override suspend fun summarize(text: String): Result<String> = runCatching {
|
|
62
|
+
val model = vertexAi.generativeModel("gemini-2.0-flash")
|
|
63
|
+
val response = model.generateContent("Summarize: $text")
|
|
64
|
+
response.text ?: throw IllegalStateException("No response from cloud model")
|
|
65
|
+
}
|
|
66
|
+
override fun isAvailable(): Flow<Boolean> = flowOf(true) // Cloud always available (with network)
|
|
67
|
+
}
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
Source: https://developer.android.com/ai/aicore
|
|
71
|
+
`;
|
|
72
|
+
const setup = `
|
|
73
|
+
# On-Device AI — Setup & Initialization
|
|
74
|
+
Source: https://developer.android.com/ml-kit/genai/on-device
|
|
75
|
+
|
|
76
|
+
## Dependencies
|
|
77
|
+
|
|
78
|
+
\`\`\`toml
|
|
79
|
+
# libs.versions.toml
|
|
80
|
+
[versions]
|
|
81
|
+
mlkit-genai = "0.1.1" # Check for latest at https://developers.google.com/ml-kit/genai
|
|
82
|
+
|
|
83
|
+
[libraries]
|
|
84
|
+
mlkit-genai-inference = { group = "com.google.mlkit", name = "genai-common", version.ref = "mlkit-genai" }
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
\`\`\`kotlin
|
|
88
|
+
// build.gradle.kts
|
|
89
|
+
implementation(libs.mlkit.genai.inference)
|
|
90
|
+
\`\`\`
|
|
91
|
+
|
|
92
|
+
## Check Model Availability Before Use
|
|
93
|
+
|
|
94
|
+
On-device models are only available on supported Pixel devices with the model downloaded.
|
|
95
|
+
Always check availability and provide a fallback.
|
|
96
|
+
|
|
97
|
+
\`\`\`kotlin
|
|
98
|
+
// Check if on-device AI is available on this device
|
|
99
|
+
class AiRepositoryFactory @Inject constructor(
|
|
100
|
+
@ApplicationContext private val context: Context,
|
|
101
|
+
private val cloudRepository: CloudAiRepository
|
|
102
|
+
) {
|
|
103
|
+
suspend fun create(): AiTextRepository {
|
|
104
|
+
return if (isOnDeviceAvailable()) {
|
|
105
|
+
val model = GenerativeModel.getInstance(context)
|
|
106
|
+
OnDeviceAiRepository(model)
|
|
107
|
+
} else {
|
|
108
|
+
cloudRepository // Graceful fallback to cloud
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private suspend fun isOnDeviceAvailable(): Boolean {
|
|
113
|
+
return try {
|
|
114
|
+
GenerativeModel.isAvailable(context)
|
|
115
|
+
} catch (e: Exception) {
|
|
116
|
+
false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
\`\`\`
|
|
121
|
+
|
|
122
|
+
## ViewModel Integration
|
|
123
|
+
|
|
124
|
+
\`\`\`kotlin
|
|
125
|
+
@HiltViewModel
|
|
126
|
+
class SummaryViewModel @Inject constructor(
|
|
127
|
+
private val aiRepository: AiTextRepository // Interface — doesn't know if on-device or cloud
|
|
128
|
+
) : ViewModel() {
|
|
129
|
+
|
|
130
|
+
private val _uiState = MutableStateFlow<SummaryUiState>(SummaryUiState.Idle)
|
|
131
|
+
val uiState: StateFlow<SummaryUiState> = _uiState.asStateFlow()
|
|
132
|
+
|
|
133
|
+
fun summarize(text: String) {
|
|
134
|
+
viewModelScope.launch {
|
|
135
|
+
_uiState.value = SummaryUiState.Loading
|
|
136
|
+
aiRepository.summarize(text)
|
|
137
|
+
.onSuccess { summary -> _uiState.value = SummaryUiState.Success(summary) }
|
|
138
|
+
.onFailure { error -> _uiState.value = SummaryUiState.Error(error.message) }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
sealed interface SummaryUiState {
|
|
144
|
+
data object Idle : SummaryUiState
|
|
145
|
+
data object Loading : SummaryUiState
|
|
146
|
+
data class Success(val summary: String) : SummaryUiState
|
|
147
|
+
data class Error(val message: String?) : SummaryUiState
|
|
148
|
+
}
|
|
149
|
+
\`\`\`
|
|
150
|
+
|
|
151
|
+
Source: https://developer.android.com/ml-kit/genai/on-device
|
|
152
|
+
`;
|
|
153
|
+
const smartReply = `
|
|
154
|
+
# On-Device AI — Smart Reply Pattern (Gmail-Style)
|
|
155
|
+
Source: https://developers.google.com/ml-kit/genai/on-device
|
|
156
|
+
|
|
157
|
+
## Smart Reply Implementation
|
|
158
|
+
|
|
159
|
+
\`\`\`kotlin
|
|
160
|
+
// Domain interface
|
|
161
|
+
interface SmartReplyRepository {
|
|
162
|
+
suspend fun getSuggestedReplies(conversationHistory: List<Message>): Result<List<String>>
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// On-device implementation using ML Kit
|
|
166
|
+
class OnDeviceSmartReplyRepository @Inject constructor(
|
|
167
|
+
private val generativeModel: GenerativeModel
|
|
168
|
+
) : SmartReplyRepository {
|
|
169
|
+
|
|
170
|
+
override suspend fun getSuggestedReplies(
|
|
171
|
+
conversationHistory: List<Message>
|
|
172
|
+
): Result<List<String>> = runCatching {
|
|
173
|
+
val prompt = buildPrompt(conversationHistory)
|
|
174
|
+
val response = generativeModel.generateContent(prompt)
|
|
175
|
+
|
|
176
|
+
// Parse the structured response into reply options
|
|
177
|
+
response.text
|
|
178
|
+
?.lines()
|
|
179
|
+
?.filter { it.isNotBlank() }
|
|
180
|
+
?.take(3) // Limit to 3 suggestions like Gmail
|
|
181
|
+
?: emptyList()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private fun buildPrompt(history: List<Message>): String {
|
|
185
|
+
val conversation = history.takeLast(5).joinToString("\n") { msg ->
|
|
186
|
+
"\${msg.sender}: \${msg.text}"
|
|
187
|
+
}
|
|
188
|
+
return """
|
|
189
|
+
Given this conversation:
|
|
190
|
+
$conversation
|
|
191
|
+
|
|
192
|
+
Suggest 3 short, natural reply options (one per line, no numbering, max 10 words each):
|
|
193
|
+
""".trimIndent()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// UI — chips that appear above keyboard
|
|
198
|
+
@Composable
|
|
199
|
+
fun SmartReplySuggestions(
|
|
200
|
+
suggestions: List<String>,
|
|
201
|
+
onSuggestionClick: (String) -> Unit
|
|
202
|
+
) {
|
|
203
|
+
if (suggestions.isEmpty()) return
|
|
204
|
+
|
|
205
|
+
LazyRow(
|
|
206
|
+
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
207
|
+
contentPadding = PaddingValues(horizontal = 16.dp)
|
|
208
|
+
) {
|
|
209
|
+
items(suggestions) { suggestion ->
|
|
210
|
+
SuggestionChip(
|
|
211
|
+
onClick = { onSuggestionClick(suggestion) },
|
|
212
|
+
label = { Text(suggestion) }
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
\`\`\`
|
|
218
|
+
|
|
219
|
+
Source: https://developers.google.com/ml-kit/genai
|
|
220
|
+
`;
|
|
221
|
+
const mlKit = `
|
|
222
|
+
# ML Kit — On-Device ML (Non-Generative)
|
|
223
|
+
Source: https://developers.google.com/ml-kit
|
|
224
|
+
|
|
225
|
+
## ML Kit vs AICore — When to Use Which
|
|
226
|
+
|
|
227
|
+
| Use Case | Tool |
|
|
228
|
+
|----------|------|
|
|
229
|
+
| Text classification, entity extraction | ML Kit Text APIs |
|
|
230
|
+
| Image labeling, object detection | ML Kit Vision APIs |
|
|
231
|
+
| Face detection, pose estimation | ML Kit Vision APIs |
|
|
232
|
+
| Barcode/QR scanning | ML Kit Barcode Scanning |
|
|
233
|
+
| Translation | ML Kit Translation |
|
|
234
|
+
| Free-form text generation, summarization, Q&A | AICore / ML Kit Gen AI |
|
|
235
|
+
|
|
236
|
+
## Common ML Kit Setup
|
|
237
|
+
|
|
238
|
+
\`\`\`toml
|
|
239
|
+
[versions]
|
|
240
|
+
mlkit = "19.0.3"
|
|
241
|
+
|
|
242
|
+
[libraries]
|
|
243
|
+
# Pick only what you need — each is a separate dependency
|
|
244
|
+
mlkit-text-recognition = { group = "com.google.mlkit", name = "text-recognition", version.ref = "mlkit" }
|
|
245
|
+
mlkit-image-labeling = { group = "com.google.mlkit", name = "image-labeling", version.ref = "mlkit" }
|
|
246
|
+
mlkit-barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkit" }
|
|
247
|
+
mlkit-face-detection = { group = "com.google.mlkit", name = "face-detection", version.ref = "mlkit" }
|
|
248
|
+
mlkit-translation = { group = "com.google.mlkit", name = "translate", version.ref = "mlkit" }
|
|
249
|
+
\`\`\`
|
|
250
|
+
|
|
251
|
+
## Architecture Pattern — ML Models Behind Repository Interfaces
|
|
252
|
+
|
|
253
|
+
\`\`\`kotlin
|
|
254
|
+
// SAME pattern as AICore — ML models are implementation details, not domain concerns
|
|
255
|
+
interface ImageAnalysisRepository {
|
|
256
|
+
suspend fun labelImage(bitmap: Bitmap): Result<List<String>>
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
class MlKitImageAnalysisRepository @Inject constructor() : ImageAnalysisRepository {
|
|
260
|
+
private val labeler = ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS)
|
|
261
|
+
|
|
262
|
+
override suspend fun labelImage(bitmap: Bitmap): Result<List<String>> = runCatching {
|
|
263
|
+
val inputImage = InputImage.fromBitmap(bitmap, 0)
|
|
264
|
+
labeler.process(inputImage).await()
|
|
265
|
+
.filter { it.confidence > 0.7f }
|
|
266
|
+
.map { it.text }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
\`\`\`
|
|
270
|
+
|
|
271
|
+
Source: https://developers.google.com/ml-kit
|
|
272
|
+
`;
|
|
273
|
+
if (t.includes("smart reply") || t.includes("suggestion"))
|
|
274
|
+
return smartReply;
|
|
275
|
+
if (t.includes("setup") || t.includes("install") || t.includes("depend") || t.includes("init"))
|
|
276
|
+
return setup;
|
|
277
|
+
if (t.includes("ml kit") || t.includes("mlkit") || t.includes("vision") || t.includes("barcode") || t.includes("label"))
|
|
278
|
+
return mlKit;
|
|
279
|
+
return overview + "\n\n---\n\n" +
|
|
280
|
+
"**Query topics:** 'setup' (dependencies + initialization), 'smart reply' (Gmail-style suggestions), " +
|
|
281
|
+
"'ml kit' (non-generative on-device ML — vision, text, barcode)\n\n" +
|
|
282
|
+
"Source: https://developer.android.com/ai";
|
|
283
|
+
}
|