Merge pull request 'Fix 1.16.6: icon chips + draw perf' (#8) from fix/1.16.6-icons-perf into main
CI / skip-ci-check (push) Successful in 29s
CI / secret-scan (push) Successful in 28s

This commit was merged in pull request #8.
This commit is contained in:
2026-08-05 13:42:46 -05:00
9 changed files with 116 additions and 121 deletions
+28 -28
View File
@@ -35,21 +35,21 @@ Do not start OCR / STT / dialogue memory until that loop works on a phone.
## 2025-10-15 - Session 4: Testing & Makefile ## 2025-10-15 - Session 4: Testing & Makefile
**Completed:** **Completed:**
- Successfully tested Crkl on Android emulator - Successfully tested Crkl on Android emulator
- Verified touch detection working perfectly - Verified touch detection working perfectly
- Created comprehensive Makefile with 50+ commands - Created comprehensive Makefile with 50+ commands
- Created MAKEFILE_GUIDE.md for reference - Created MAKEFILE_GUIDE.md for reference
- Created CHEATSHEET.md for quick commands - Created CHEATSHEET.md for quick commands
- Created DEMO_GUIDE.md for presenting - Created DEMO_GUIDE.md for presenting
- Created TEST_RESULTS.md with full validation - Created TEST_RESULTS.md with full validation
- Created HOW_TO_TEST.md step-by-step guide - Created HOW_TO_TEST.md step-by-step guide
**Test Results:** **Test Results:**
- Touch detection: PASS (4/4 test points) - Touch detection: PASS (4/4 test points)
- Accessibility service: PASS - Accessibility service: PASS
- System-wide overlay: PASS - System-wide overlay: PASS
- Logging: PASS - Logging: PASS
- Performance: PASS (< 16ms latency) - Performance: PASS (< 16ms latency)
- **POC Status: 100% COMPLETE** - **POC Status: 100% COMPLETE**
**Key Achievements:** **Key Achievements:**
@@ -70,13 +70,13 @@ Do not start OCR / STT / dialogue memory until that loop works on a phone.
## 2025-10-15 - Session 3: Cursor AI Optimization ## 2025-10-15 - Session 3: Cursor AI Optimization
**Completed:** **Completed:**
- Enhanced `.cursorrules` with comprehensive AI guidelines - Enhanced `.cursorrules` with comprehensive AI guidelines
- Improved `.cursorignore` to exclude all build artifacts - Improved `.cursorignore` to exclude all build artifacts
- Created `TESTING_ON_LINUX.md` - Complete Linux testing guide - Created `TESTING_ON_LINUX.md` - Complete Linux testing guide
- Created `PROJECT_REVIEW.md` - Comprehensive project analysis - Created `PROJECT_REVIEW.md` - Comprehensive project analysis
- Added `scripts/setup_emulator.sh` - One-command emulator setup - Added `scripts/setup_emulator.sh` - One-command emulator setup
- Verified project follows Cursor AI best practices - Verified project follows Cursor AI best practices
- Documented all testing options (emulator, unit tests, instrumentation) - Documented all testing options (emulator, unit tests, instrumentation)
**Key Insights:** **Key Insights:**
- **No phone needed!** Can test entirely on Linux with Android emulator - **No phone needed!** Can test entirely on Linux with Android emulator
@@ -99,17 +99,17 @@ Do not start OCR / STT / dialogue memory until that loop works on a phone.
## 2025-10-15 - Session 2: POC Implementation ## 2025-10-15 - Session 2: POC Implementation
**Completed:** **Completed:**
- Installed Android SDK command-line tools on Linux - Installed Android SDK command-line tools on Linux
- Created proper Android project structure (Gradle, Kotlin, Jetpack Compose) - Created proper Android project structure (Gradle, Kotlin, Jetpack Compose)
- Implemented MainActivity with onboarding UI - Implemented MainActivity with onboarding UI
- Created AccessibilityService with system-wide overlay - Created AccessibilityService with system-wide overlay
- Built OverlayView with basic touch detection - Built OverlayView with basic touch detection
- Successfully compiled and generated APK - Successfully compiled and generated APK
- Created comprehensive testing documentation - Created comprehensive testing documentation
**Current Status:** **Current Status:**
- **Phase:** POC (Proof of Concept) - Milestone 1 - **Phase:** POC (Proof of Concept) - Milestone 1
- **Build:** SUCCESS - **Build:** SUCCESS
- **APK:** `app/build/outputs/apk/debug/app-debug.apk` (15MB) - **APK:** `app/build/outputs/apk/debug/app-debug.apk` (15MB)
- **Functionality:** Basic overlay with touch visualization - **Functionality:** Basic overlay with touch visualization
+5
View File
@@ -1,5 +1,10 @@
# Changelog # Changelog
## 1.16.6-icons-perf — 2026-08-05
- VIP actions are icon-only (Todo = checkbox with checkmark)
- Perf: drop software layer + BlurMaskFilter; draw stroke via Path; throttle points
## 1.16.5-chip-scroll — 2026-08-05 ## 1.16.5-chip-scroll — 2026-08-05
- Action chips: icon + label; horizontal scroll actually works (child was MATCH_PARENT and clipped) - Action chips: icon + label; horizontal scroll actually works (child was MATCH_PARENT and clipped)
+6 -6
View File
@@ -134,12 +134,12 @@ Refer to `.notes/directory_structure.md` for module organization:
**Critical:** All contributions must adhere to privacy-first principles: **Critical:** All contributions must adhere to privacy-first principles:
- **DO:** Use local-only processing - DO: Use local-only processing
- **DO:** Keep all data on device - DO: Keep all data on device
- **DO:** Use offline ML models - DO: Use offline ML models
- **DON'T:** Add network calls for AI features - DON'T: Add network calls for AI features
- **DON'T:** Send data to external services - DON'T: Send data to external services
- **DON'T:** Include cloud service dependencies - DON'T: Include cloud service dependencies
## Review Process ## Review Process
+9 -8
View File
@@ -1,6 +1,7 @@
# Circle # Circle
Privacy-first Android assist: **circle on-screen text Translate · Copy · Explain · Share · Vikunja**. On-device. Android assist app: circle on-screen text, then Translate, Copy, Explain, Share,
or Vikunja. On-device processing.
Repo / Gradle module: **crkl** · Application id: `com.example.crkl` · Display name: **Circle** Repo / Gradle module: **crkl** · Application id: `com.example.crkl` · Display name: **Circle**
@@ -17,14 +18,14 @@ Repo / Gradle module: **crkl** · Application id: `com.example.crkl` · Display
## Features ## Features
- **Circle → text → act** (a11y + on-device OCR) - Circle gesture captures on-screen text (accessibility service + on-device OCR)
- Chips: **Translate · Copy · Explain · Share · Vikunja** - Action chips: Translate, Copy, Explain, Share, Vikunja
- **Vikunja** HTTPS for add to todo - Vikunja HTTPS integration for add-to-todo
- Optional mailto / lab gog bridge (Advanced in Integrations) - Optional mailto / lab gog bridge (Advanced in Integrations)
- Device calendar read (Advanced) - Device calendar read (Advanced)
- Circle style (color, neon, stroke) - Circle style settings (color, neon, stroke)
- First-run onboarding; clean panel (debug meta optional) - First-run onboarding; optional debug metadata in panel
- Shortcuts: Quick Settings **Circle** tile, Accessibility button, volume-key a11y shortcut - Shortcuts: Quick Settings Circle tile, Accessibility button, volume-key shortcut
- Local LLM when a `.task` model is present; otherwise stub Explain / summary - Local LLM when a `.task` model is present; otherwise stub Explain / summary
## Quick start ## Quick start
@@ -91,4 +92,4 @@ Inference is MediaPipe on-device (`make push-model`). Without a model, Circle us
## License ## License
See repository license file. MIT — see [LICENSE](LICENSE).
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "com.example.crkl" applicationId = "com.example.crkl"
minSdk = 27 minSdk = 27
targetSdk = 34 targetSdk = 34
versionCode = 22 versionCode = 23
versionName = "1.16.5-chip-scroll" versionName = "1.16.6-icons-perf"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@@ -1,10 +1,10 @@
package com.example.crkl.accessibility package com.example.crkl.accessibility
import android.content.Context import android.content.Context
import android.graphics.BlurMaskFilter
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Path
import android.graphics.PointF import android.graphics.PointF
import android.graphics.RectF import android.graphics.RectF
import android.os.Build import android.os.Build
@@ -41,8 +41,9 @@ class OverlayView(
strokeWidth = dp(2.5f) strokeWidth = dp(2.5f)
} }
private val buttonShadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val buttonShadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#55000000") // Soft disc — BlurMaskFilter forced LAYER_TYPE_SOFTWARE and lagged the whole UI.
maskFilter = BlurMaskFilter(dp(10f), BlurMaskFilter.Blur.NORMAL) color = Color.parseColor("#33000000")
style = Paint.Style.FILL
} }
private var isOverlayMode = false private var isOverlayMode = false
@@ -69,8 +70,10 @@ class OverlayView(
private var isDrawing = false private var isDrawing = false
private var touchPath = mutableListOf<PointF>() private var touchPath = mutableListOf<PointF>()
private val strokePath = Path()
private var hintMessage = "Draw a closed loop around text" private var hintMessage = "Draw a closed loop around text"
private var failFlashUntil = 0L private var failFlashUntil = 0L
private val minPointSpacingPx = dp(3f)
private val glowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val glowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE style = Paint.Style.STROKE
@@ -117,7 +120,8 @@ class OverlayView(
private var touchStartedInExit = false private var touchStartedInExit = false
init { init {
setLayerType(LAYER_TYPE_SOFTWARE, null) // Hardware layer — software + BlurMaskFilter made draw/scroll feel sticky.
setLayerType(LAYER_TYPE_HARDWARE, null)
setBackgroundColor(Color.TRANSPARENT) setBackgroundColor(Color.TRANSPARENT)
isClickable = true isClickable = true
isFocusable = false isFocusable = false
@@ -134,14 +138,11 @@ class OverlayView(
neonEnabled = neon neonEnabled = neon
pathPaint.color = colorArgb pathPaint.color = colorArgb
pathPaint.strokeWidth = dp(strokeDp) pathPaint.strokeWidth = dp(strokeDp)
// Fake glow with a wider translucent stroke — no BlurMaskFilter.
glowPaint.color = colorArgb glowPaint.color = colorArgb
glowPaint.strokeWidth = dp(strokeDp + 6f) glowPaint.strokeWidth = dp(strokeDp + 5f)
glowPaint.alpha = if (neon) 120 else 0 glowPaint.alpha = if (neon) 70 else 0
glowPaint.maskFilter = if (neon) { glowPaint.maskFilter = null
BlurMaskFilter(dp(12f), BlurMaskFilter.Blur.NORMAL)
} else {
null
}
invalidate() invalidate()
} }
@@ -151,8 +152,7 @@ class OverlayView(
fun enterOverlayMode() { fun enterOverlayMode() {
if (isOverlayMode) return if (isOverlayMode) return
isOverlayMode = true isOverlayMode = true
touchPath.clear() clearStroke()
selectionBounds = null
touchStartedInExit = false touchStartedInExit = false
successFlash = false successFlash = false
hintMessage = "Draw a closed loop around text" hintMessage = "Draw a closed loop around text"
@@ -164,8 +164,7 @@ class OverlayView(
fun exitOverlayMode() { fun exitOverlayMode() {
if (!isOverlayMode) return if (!isOverlayMode) return
isOverlayMode = false isOverlayMode = false
touchPath.clear() clearStroke()
selectionBounds = null
touchStartedInExit = false touchStartedInExit = false
successFlash = false successFlash = false
hintMessage = "Draw a closed loop around text" hintMessage = "Draw a closed loop around text"
@@ -175,8 +174,7 @@ class OverlayView(
private fun toggleOverlayMode() { private fun toggleOverlayMode() {
isOverlayMode = !isOverlayMode isOverlayMode = !isOverlayMode
touchPath.clear() clearStroke()
selectionBounds = null
touchStartedInExit = false touchStartedInExit = false
successFlash = false successFlash = false
hintMessage = "Draw a closed loop around text" hintMessage = "Draw a closed loop around text"
@@ -187,6 +185,25 @@ class OverlayView(
invalidate() invalidate()
} }
private fun clearStroke() {
touchPath.clear()
strokePath.reset()
selectionBounds = null
}
private fun appendStrokePoint(x: Float, y: Float) {
val last = touchPath.lastOrNull()
if (last != null) {
val dx = x - last.x
val dy = y - last.y
if (dx * dx + dy * dy < minPointSpacingPx * minPointSpacingPx) return
strokePath.lineTo(x, y)
} else {
strokePath.moveTo(x, y)
}
touchPath.add(PointF(x, y))
}
override fun onTouchEvent(event: MotionEvent?): Boolean { override fun onTouchEvent(event: MotionEvent?): Boolean {
if (!isOverlayMode) { if (!isOverlayMode) {
return super.onTouchEvent(event) return super.onTouchEvent(event)
@@ -201,13 +218,12 @@ class OverlayView(
if (touchStartedInExit) return true if (touchStartedInExit) return true
isDrawing = true isDrawing = true
successFlash = false successFlash = false
touchPath.clear() clearStroke()
selectionBounds = null appendStrokePoint(motionEvent.x, motionEvent.y)
touchPath.add(PointF(motionEvent.x, motionEvent.y))
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
if (!touchStartedInExit && isDrawing) { if (!touchStartedInExit && isDrawing) {
touchPath.add(PointF(motionEvent.x, motionEvent.y)) appendStrokePoint(motionEvent.x, motionEvent.y)
} }
} }
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
@@ -317,7 +333,7 @@ class OverlayView(
val cx = width / 2f val cx = width / 2f
val cy = height / 2f val cy = height / 2f
val r = buttonSizePx / 2f val r = buttonSizePx / 2f
canvas.drawCircle(cx, cy + dp(2f), r, buttonShadowPaint) canvas.drawCircle(cx, cy + dp(2f), r + dp(1.5f), buttonShadowPaint)
canvas.drawCircle(cx, cy, r, buttonFillPaint) canvas.drawCircle(cx, cy, r, buttonFillPaint)
canvas.drawCircle(cx, cy, r - dp(3f), buttonRingPaint) canvas.drawCircle(cx, cy, r - dp(3f), buttonRingPaint)
canvas.drawText("C", cx, cy + textPaint.textSize / 3f, textPaint) canvas.drawText("C", cx, cy + textPaint.textSize / 3f, textPaint)
@@ -355,15 +371,11 @@ class OverlayView(
exitTextPaint exitTextPaint
) )
if (touchPath.size > 1) { if (!strokePath.isEmpty) {
for (i in 1 until touchPath.size) { if (neonEnabled && glowPaint.alpha > 0) {
val prev = touchPath[i - 1] canvas.drawPath(strokePath, glowPaint)
val curr = touchPath[i]
if (neonEnabled) {
canvas.drawLine(prev.x, prev.y, curr.x, curr.y, glowPaint)
}
canvas.drawLine(prev.x, prev.y, curr.x, curr.y, pathPaint)
} }
canvas.drawPath(strokePath, pathPaint)
} }
selectionBounds?.let { bounds -> selectionBounds?.let { bounds ->
@@ -10,6 +10,8 @@ import android.view.Gravity
import android.view.View import android.view.View
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import android.widget.HorizontalScrollView import android.widget.HorizontalScrollView
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
@@ -130,7 +132,6 @@ class ResultPanelView(
val actionsRow = LinearLayout(context).apply { val actionsRow = LinearLayout(context).apply {
orientation = HORIZONTAL orientation = HORIZONTAL
gravity = Gravity.CENTER_VERTICAL gravity = Gravity.CENTER_VERTICAL
setPadding(0, 0, dp(28), 0) // peek next chip past the fade
} }
fun actionChip( fun actionChip(
@@ -138,26 +139,20 @@ class ResultPanelView(
iconRes: Int, iconRes: Int,
bg: Int, bg: Int,
onClick: () -> Unit onClick: () -> Unit
): TextView { ): ImageButton {
val icon = context.getDrawable(iconRes)?.mutate()?.apply { val size = dp(44)
setBounds(0, 0, dp(16), dp(16)) return ImageButton(context).apply {
} setImageResource(iconRes)
return TextView(context).apply { imageTintList = android.content.res.ColorStateList.valueOf(CrklUi.ChipFg)
text = label scaleType = ImageView.ScaleType.CENTER_INSIDE
setTextColor(CrklUi.ChipFg) setPadding(dp(10), dp(10), dp(10), dp(10))
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12.5f)
typeface = Typeface.DEFAULT_BOLD
setCompoundDrawables(icon, null, null, null)
compoundDrawablePadding = dp(6)
gravity = Gravity.CENTER_VERTICAL
setPadding(dp(10), dp(9), dp(12), dp(9))
background = GradientDrawable().apply { background = GradientDrawable().apply {
setColor(bg) setColor(bg)
cornerRadius = dp(18).toFloat() cornerRadius = dp(22).toFloat()
}
layoutParams = LayoutParams(size, size).apply {
marginEnd = dp(8)
} }
val lp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
lp.marginEnd = dp(6)
layoutParams = lp
isClickable = true isClickable = true
isFocusable = true isFocusable = true
contentDescription = label contentDescription = label
@@ -193,7 +188,7 @@ class ResultPanelView(
} }
if (onAddTodo != null) { if (onAddTodo != null) {
actionsRow.addView( actionsRow.addView(
actionChip("Vikunja", R.drawable.ic_chip_todo, CrklUi.Ok, onAddTodo) actionChip("Todo", R.drawable.ic_chip_todo, CrklUi.Ok, onAddTodo)
) )
} }
@@ -207,25 +202,15 @@ class ResultPanelView(
return super.onInterceptTouchEvent(ev) return super.onInterceptTouchEvent(ev)
} }
}.apply { }.apply {
isHorizontalScrollBarEnabled = true isHorizontalScrollBarEnabled = false
isScrollbarFadingEnabled = false
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
overScrollMode = OVER_SCROLL_IF_CONTENT_SCROLLS overScrollMode = OVER_SCROLL_IF_CONTENT_SCROLLS
isFillViewport = false isFillViewport = false
clipToPadding = false clipToPadding = false
setPadding(0, dp(8), dp(4), dp(6)) setPadding(0, dp(8), dp(8), dp(4))
// WRAP_CONTENT is required — MATCH_PARENT (default) clips chips instead of scrolling.
addView( addView(
actionsRow, actionsRow,
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
) )
// Nudge once so the first overflow chip peeks (affordance).
post {
if (actionsRow.measuredWidth > width && scrollX == 0) {
smoothScrollBy(dp(28), 0)
postDelayed({ smoothScrollTo(0, 0) }, 450)
}
}
} }
listenView = TextView(context).apply { listenView = TextView(context).apply {
+5 -1
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Filled checkbox + check — stroke-only vectors are unreliable on older APIs -->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
@@ -6,5 +7,8 @@
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M6,5h12v2H6V5zM6,11h12v2H6v-2zM6,17h8v2H6v-2zM18.5,16.2l1.4,1.4 -3.2,3.2 -2,-2 1.4,-1.4 0.6,0.6 1.8,-1.8z" /> android:pathData="M6,3h12c1.7,0 3,1.3 3,3v12c0,1.7 -1.3,3 -3,3H6c-1.7,0 -3,-1.3 -3,-3V6c0,-1.7 1.3,-3 3,-3zM7.5,5.5c-0.8,0 -1.5,0.7 -1.5,1.5v10c0,0.8 0.7,1.5 1.5,1.5h9c0.8,0 1.5,-0.7 1.5,-1.5V7c0,-0.8 -0.7,-1.5 -1.5,-1.5h-9z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M10.3,15.7L7,12.4l1.4,-1.4 1.9,1.9 4.9,-4.9 1.4,1.4z" />
</vector> </vector>
+3 -15
View File
@@ -147,30 +147,18 @@ echo "$logs" | grep -qE 'extract:.*chars=[1-9]' || die "case2: no text extracted
pass "email region extract" pass "email region extract"
"$ADB" logcat -c "$ADB" logcat -c
# Chip row ~y=2100 on 1080x2400 with 54% sheet; Copy sits between Translate and Explain. # Icon-only chips: Translate, Copy, Explain, Share, Todo — ~44dp, y≈2100.
copied=0 copied=0
for y in 2090 2100 2110; do for y in 2090 2100 2110; do
for x in 340 360 320 300 380; do for x in 200 240 280 160 320; do
"$ADB" shell input tap "$x" "$y" "$ADB" shell input tap "$x" "$y"
sleep 0.35 sleep 0.3
if "$ADB" logcat -d | grep -q 'action kind=COPY'; then if "$ADB" logcat -d | grep -q 'action kind=COPY'; then
copied=1 copied=1
break 2 break 2
fi fi
done done
done done
if [[ "$copied" != 1 ]]; then
"$ADB" shell input swipe 200 2100 700 2100 200
sleep 0.3
for x in 340 360 320; do
"$ADB" shell input tap "$x" 2100
sleep 0.35
if "$ADB" logcat -d | grep -q 'action kind=COPY'; then
copied=1
break
fi
done
fi
[[ "$copied" = 1 ]] || die "Copy chip not triggered" [[ "$copied" = 1 ]] || die "Copy chip not triggered"
"$ADB" logcat -d | grep -q 'action result ok=true' || die "Copy failed" "$ADB" logcat -d | grep -q 'action result ok=true' || die "Copy failed"
pass "Copy chip" pass "Copy chip"