From 529504e89b1bde184ed4e4e93f307bd241d2d4c8 Mon Sep 17 00:00:00 2001 From: JordanTheJet Date: Fri, 20 Feb 2026 16:41:36 -0500 Subject: [PATCH] Add Quick Settings tile for one-tap screenshot & explain - New CellClawTileService with @EntryPoint for Hilt, tile state reflecting AgentState (ACTIVE/INACTIVE labels), and onClick screenshot+explain flow posting result to CHANNEL_ALERTS notification (ID 200) - New ic_qs_cellclaw.xml 24dp monochrome vector for QS tile icon - Register CellClawTileService in manifest with BIND_QUICK_SETTINGS_TILE permission, QS_TILE intent-filter, and ACTIVE_TILE metadata Co-Authored-By: Claude Opus 4.6 --- app/src/main/AndroidManifest.xml | 15 ++ .../cellclaw/service/CellClawTileService.kt | 177 ++++++++++++++++++ app/src/main/res/drawable/ic_qs_cellclaw.xml | 11 ++ 3 files changed, 203 insertions(+) create mode 100644 app/src/main/kotlin/com/cellclaw/service/CellClawTileService.kt create mode 100644 app/src/main/res/drawable/ic_qs_cellclaw.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2d782b4..13a0388 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -103,6 +103,21 @@ android:resource="@xml/accessibility_config" /> + + + + + + + + + (data as? JsonObject)?.get("file_path") + ?.let { (it as? JsonPrimitive)?.content } + } + if (filePath == null) { + postResultNotification("Screenshot failed: no file path") + restoreTile() + return@launch + } + + val analyzeResult = visionAnalyze.execute(buildJsonObject { + put("file_path", filePath) + put("question", "Describe what you see on the screen. What app is open? What is the user looking at?") + }) + + if (analyzeResult.success) { + val analysis = analyzeResult.data?.let { data -> + (data as? JsonObject)?.get("analysis") + ?.let { (it as? JsonPrimitive)?.content } + } ?: "Analysis complete" + postResultNotification(analysis) + } else { + postResultNotification("Analysis failed: ${analyzeResult.error}") + } + } catch (e: Exception) { + Log.e(TAG, "Tile screenshot+explain failed: ${e.message}") + postResultNotification("Error: ${e.message}") + } finally { + restoreTile() + } + } + } + + override fun onDestroy() { + tileScope.cancel() + super.onDestroy() + } + + private fun updateTileState() { + val tile = qsTile ?: return + try { + val agentLoop = getEntryPoint().agentLoop() + when (agentLoop.state.value) { + AgentState.IDLE -> { + tile.state = Tile.STATE_ACTIVE + tile.label = "CellClaw" + } + AgentState.THINKING -> { + tile.state = Tile.STATE_ACTIVE + tile.label = "Thinking..." + } + AgentState.EXECUTING_TOOLS -> { + tile.state = Tile.STATE_ACTIVE + tile.label = "Working..." + } + AgentState.WAITING_APPROVAL -> { + tile.state = Tile.STATE_ACTIVE + tile.label = "Approval" + } + AgentState.PAUSED -> { + tile.state = Tile.STATE_INACTIVE + tile.label = "Paused" + } + AgentState.ERROR -> { + tile.state = Tile.STATE_INACTIVE + tile.label = "Error" + } + } + } catch (e: Exception) { + tile.state = Tile.STATE_INACTIVE + tile.label = "CellClaw" + } + tile.updateTile() + } + + private fun restoreTile() { + try { + updateTileState() + } catch (_: Exception) {} + } + + private fun postResultNotification(text: String) { + val notification = NotificationCompat.Builder(this, CellClawApp.CHANNEL_ALERTS) + .setContentTitle("Screen Analysis") + .setContentText(text) + .setStyle(NotificationCompat.BigTextStyle().bigText(text)) + .setSmallIcon(R.drawable.ic_notification) + .setAutoCancel(true) + .build() + + val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + manager.notify(TILE_NOTIFICATION_ID, notification) + } + + companion object { + private const val TAG = "CellClawTile" + private const val TILE_NOTIFICATION_ID = 200 + } +} diff --git a/app/src/main/res/drawable/ic_qs_cellclaw.xml b/app/src/main/res/drawable/ic_qs_cellclaw.xml new file mode 100644 index 0000000..6da962c --- /dev/null +++ b/app/src/main/res/drawable/ic_qs_cellclaw.xml @@ -0,0 +1,11 @@ + + + +