From 8509d93ed529b397d65be9c59224955372e6371f Mon Sep 17 00:00:00 2001 From: ilia Date: Wed, 5 Aug 2026 14:40:55 -0400 Subject: [PATCH] Icon-only VIP chips and faster draw overlay Replace labeled chips with icons (Todo = checkbox+check). Drop software layer and BlurMaskFilter; draw strokes with a Path and spaced points. --- .notes/meeting_notes.md | 56 +++++++-------- CHANGELOG.md | 5 ++ CONTRIBUTING.md | 12 ++-- README.md | 17 ++--- app/build.gradle.kts | 4 +- .../example/crkl/accessibility/OverlayView.kt | 72 +++++++++++-------- .../com/example/crkl/ui/ResultPanelView.kt | 47 +++++------- app/src/main/res/drawable/ic_chip_todo.xml | 6 +- scripts/smoke-circle.sh | 18 +---- 9 files changed, 116 insertions(+), 121 deletions(-) diff --git a/.notes/meeting_notes.md b/.notes/meeting_notes.md index 6679aed..3afc421 100644 --- a/.notes/meeting_notes.md +++ b/.notes/meeting_notes.md @@ -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 **Completed:** -- ✅ Successfully tested Crkl on Android emulator -- ✅ Verified touch detection working perfectly -- ✅ Created comprehensive Makefile with 50+ commands -- ✅ Created MAKEFILE_GUIDE.md for reference -- ✅ Created CHEATSHEET.md for quick commands -- ✅ Created DEMO_GUIDE.md for presenting -- ✅ Created TEST_RESULTS.md with full validation -- ✅ Created HOW_TO_TEST.md step-by-step guide +- Successfully tested Crkl on Android emulator +- Verified touch detection working perfectly +- Created comprehensive Makefile with 50+ commands +- Created MAKEFILE_GUIDE.md for reference +- Created CHEATSHEET.md for quick commands +- Created DEMO_GUIDE.md for presenting +- Created TEST_RESULTS.md with full validation +- Created HOW_TO_TEST.md step-by-step guide **Test Results:** -- ✅ Touch detection: PASS (4/4 test points) -- ✅ Accessibility service: PASS -- ✅ System-wide overlay: PASS -- ✅ Logging: PASS -- ✅ Performance: PASS (< 16ms latency) +- Touch detection: PASS (4/4 test points) +- Accessibility service: PASS +- System-wide overlay: PASS +- Logging: PASS +- Performance: PASS (< 16ms latency) - **POC Status: 100% COMPLETE** **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 **Completed:** -- ✅ Enhanced `.cursorrules` with comprehensive AI guidelines -- ✅ Improved `.cursorignore` to exclude all build artifacts -- ✅ Created `TESTING_ON_LINUX.md` - Complete Linux testing guide -- ✅ Created `PROJECT_REVIEW.md` - Comprehensive project analysis -- ✅ Added `scripts/setup_emulator.sh` - One-command emulator setup -- ✅ Verified project follows Cursor AI best practices -- ✅ Documented all testing options (emulator, unit tests, instrumentation) +- Enhanced `.cursorrules` with comprehensive AI guidelines +- Improved `.cursorignore` to exclude all build artifacts +- Created `TESTING_ON_LINUX.md` - Complete Linux testing guide +- Created `PROJECT_REVIEW.md` - Comprehensive project analysis +- Added `scripts/setup_emulator.sh` - One-command emulator setup +- Verified project follows Cursor AI best practices +- Documented all testing options (emulator, unit tests, instrumentation) **Key Insights:** - **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 **Completed:** -- ✅ Installed Android SDK command-line tools on Linux -- ✅ Created proper Android project structure (Gradle, Kotlin, Jetpack Compose) -- ✅ Implemented MainActivity with onboarding UI -- ✅ Created AccessibilityService with system-wide overlay -- ✅ Built OverlayView with basic touch detection -- ✅ Successfully compiled and generated APK -- ✅ Created comprehensive testing documentation +- Installed Android SDK command-line tools on Linux +- Created proper Android project structure (Gradle, Kotlin, Jetpack Compose) +- Implemented MainActivity with onboarding UI +- Created AccessibilityService with system-wide overlay +- Built OverlayView with basic touch detection +- Successfully compiled and generated APK +- Created comprehensive testing documentation **Current Status:** - **Phase:** POC (Proof of Concept) - Milestone 1 -- **Build:** ✅ SUCCESS +- **Build:** SUCCESS - **APK:** `app/build/outputs/apk/debug/app-debug.apk` (15MB) - **Functionality:** Basic overlay with touch visualization diff --git a/CHANGELOG.md b/CHANGELOG.md index bb47c64..8f48fb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 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 - Action chips: icon + label; horizontal scroll actually works (child was MATCH_PARENT and clipped) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a40a417..391feeb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,12 +134,12 @@ Refer to `.notes/directory_structure.md` for module organization: **Critical:** All contributions must adhere to privacy-first principles: -- ✅ **DO:** Use local-only processing -- ✅ **DO:** Keep all data on device -- ✅ **DO:** Use offline ML models -- ❌ **DON'T:** Add network calls for AI features -- ❌ **DON'T:** Send data to external services -- ❌ **DON'T:** Include cloud service dependencies +- DO: Use local-only processing +- DO: Keep all data on device +- DO: Use offline ML models +- DON'T: Add network calls for AI features +- DON'T: Send data to external services +- DON'T: Include cloud service dependencies ## Review Process diff --git a/README.md b/README.md index fbb7e8a..8d34955 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # 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** @@ -17,14 +18,14 @@ Repo / Gradle module: **crkl** · Application id: `com.example.crkl` · Display ## Features -- **Circle → text → act** (a11y + on-device OCR) -- Chips: **Translate · Copy · Explain · Share · Vikunja** -- **Vikunja** HTTPS for “add to todo” +- Circle gesture captures on-screen text (accessibility service + on-device OCR) +- Action chips: Translate, Copy, Explain, Share, Vikunja +- Vikunja HTTPS integration for add-to-todo - Optional mailto / lab gog bridge (Advanced in Integrations) - Device calendar read (Advanced) -- Circle style (color, neon, stroke) -- First-run onboarding; clean panel (debug meta optional) -- Shortcuts: Quick Settings **Circle** tile, Accessibility button, volume-key a11y shortcut +- Circle style settings (color, neon, stroke) +- First-run onboarding; optional debug metadata in panel +- Shortcuts: Quick Settings Circle tile, Accessibility button, volume-key shortcut - Local LLM when a `.task` model is present; otherwise stub Explain / summary ## Quick start @@ -91,4 +92,4 @@ Inference is MediaPipe on-device (`make push-model`). Without a model, Circle us ## License -See repository license file. +MIT — see [LICENSE](LICENSE). diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 06bd2e9..dd13a38 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "com.example.crkl" minSdk = 27 targetSdk = 34 - versionCode = 22 - versionName = "1.16.5-chip-scroll" + versionCode = 23 + versionName = "1.16.6-icons-perf" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/kotlin/com/example/crkl/accessibility/OverlayView.kt b/app/src/main/kotlin/com/example/crkl/accessibility/OverlayView.kt index f4f86a6..5b9734e 100644 --- a/app/src/main/kotlin/com/example/crkl/accessibility/OverlayView.kt +++ b/app/src/main/kotlin/com/example/crkl/accessibility/OverlayView.kt @@ -1,10 +1,10 @@ package com.example.crkl.accessibility import android.content.Context -import android.graphics.BlurMaskFilter import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint +import android.graphics.Path import android.graphics.PointF import android.graphics.RectF import android.os.Build @@ -41,8 +41,9 @@ class OverlayView( strokeWidth = dp(2.5f) } private val buttonShadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = Color.parseColor("#55000000") - maskFilter = BlurMaskFilter(dp(10f), BlurMaskFilter.Blur.NORMAL) + // Soft disc — BlurMaskFilter forced LAYER_TYPE_SOFTWARE and lagged the whole UI. + color = Color.parseColor("#33000000") + style = Paint.Style.FILL } private var isOverlayMode = false @@ -69,8 +70,10 @@ class OverlayView( private var isDrawing = false private var touchPath = mutableListOf() + private val strokePath = Path() private var hintMessage = "Draw a closed loop around text" private var failFlashUntil = 0L + private val minPointSpacingPx = dp(3f) private val glowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE @@ -117,7 +120,8 @@ class OverlayView( private var touchStartedInExit = false init { - setLayerType(LAYER_TYPE_SOFTWARE, null) + // Hardware layer — software + BlurMaskFilter made draw/scroll feel sticky. + setLayerType(LAYER_TYPE_HARDWARE, null) setBackgroundColor(Color.TRANSPARENT) isClickable = true isFocusable = false @@ -134,14 +138,11 @@ class OverlayView( neonEnabled = neon pathPaint.color = colorArgb pathPaint.strokeWidth = dp(strokeDp) + // Fake glow with a wider translucent stroke — no BlurMaskFilter. glowPaint.color = colorArgb - glowPaint.strokeWidth = dp(strokeDp + 6f) - glowPaint.alpha = if (neon) 120 else 0 - glowPaint.maskFilter = if (neon) { - BlurMaskFilter(dp(12f), BlurMaskFilter.Blur.NORMAL) - } else { - null - } + glowPaint.strokeWidth = dp(strokeDp + 5f) + glowPaint.alpha = if (neon) 70 else 0 + glowPaint.maskFilter = null invalidate() } @@ -151,8 +152,7 @@ class OverlayView( fun enterOverlayMode() { if (isOverlayMode) return isOverlayMode = true - touchPath.clear() - selectionBounds = null + clearStroke() touchStartedInExit = false successFlash = false hintMessage = "Draw a closed loop around text" @@ -164,8 +164,7 @@ class OverlayView( fun exitOverlayMode() { if (!isOverlayMode) return isOverlayMode = false - touchPath.clear() - selectionBounds = null + clearStroke() touchStartedInExit = false successFlash = false hintMessage = "Draw a closed loop around text" @@ -175,8 +174,7 @@ class OverlayView( private fun toggleOverlayMode() { isOverlayMode = !isOverlayMode - touchPath.clear() - selectionBounds = null + clearStroke() touchStartedInExit = false successFlash = false hintMessage = "Draw a closed loop around text" @@ -187,6 +185,25 @@ class OverlayView( 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 { if (!isOverlayMode) { return super.onTouchEvent(event) @@ -201,13 +218,12 @@ class OverlayView( if (touchStartedInExit) return true isDrawing = true successFlash = false - touchPath.clear() - selectionBounds = null - touchPath.add(PointF(motionEvent.x, motionEvent.y)) + clearStroke() + appendStrokePoint(motionEvent.x, motionEvent.y) } MotionEvent.ACTION_MOVE -> { if (!touchStartedInExit && isDrawing) { - touchPath.add(PointF(motionEvent.x, motionEvent.y)) + appendStrokePoint(motionEvent.x, motionEvent.y) } } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { @@ -317,7 +333,7 @@ class OverlayView( val cx = width / 2f val cy = height / 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 - dp(3f), buttonRingPaint) canvas.drawText("C", cx, cy + textPaint.textSize / 3f, textPaint) @@ -355,15 +371,11 @@ class OverlayView( exitTextPaint ) - if (touchPath.size > 1) { - for (i in 1 until touchPath.size) { - val prev = touchPath[i - 1] - 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) + if (!strokePath.isEmpty) { + if (neonEnabled && glowPaint.alpha > 0) { + canvas.drawPath(strokePath, glowPaint) } + canvas.drawPath(strokePath, pathPaint) } selectionBounds?.let { bounds -> diff --git a/app/src/main/kotlin/com/example/crkl/ui/ResultPanelView.kt b/app/src/main/kotlin/com/example/crkl/ui/ResultPanelView.kt index baa36e6..eedfc5c 100644 --- a/app/src/main/kotlin/com/example/crkl/ui/ResultPanelView.kt +++ b/app/src/main/kotlin/com/example/crkl/ui/ResultPanelView.kt @@ -10,6 +10,8 @@ import android.view.Gravity import android.view.View import android.view.animation.DecelerateInterpolator import android.widget.HorizontalScrollView +import android.widget.ImageButton +import android.widget.ImageView import android.widget.LinearLayout import android.widget.ScrollView import android.widget.TextView @@ -130,7 +132,6 @@ class ResultPanelView( val actionsRow = LinearLayout(context).apply { orientation = HORIZONTAL gravity = Gravity.CENTER_VERTICAL - setPadding(0, 0, dp(28), 0) // peek next chip past the fade } fun actionChip( @@ -138,26 +139,20 @@ class ResultPanelView( iconRes: Int, bg: Int, onClick: () -> Unit - ): TextView { - val icon = context.getDrawable(iconRes)?.mutate()?.apply { - setBounds(0, 0, dp(16), dp(16)) - } - return TextView(context).apply { - text = label - setTextColor(CrklUi.ChipFg) - 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)) + ): ImageButton { + val size = dp(44) + return ImageButton(context).apply { + setImageResource(iconRes) + imageTintList = android.content.res.ColorStateList.valueOf(CrklUi.ChipFg) + scaleType = ImageView.ScaleType.CENTER_INSIDE + setPadding(dp(10), dp(10), dp(10), dp(10)) background = GradientDrawable().apply { 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 isFocusable = true contentDescription = label @@ -193,7 +188,7 @@ class ResultPanelView( } if (onAddTodo != null) { 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) } }.apply { - isHorizontalScrollBarEnabled = true - isScrollbarFadingEnabled = false - scrollBarStyle = View.SCROLLBARS_INSIDE_INSET + isHorizontalScrollBarEnabled = false overScrollMode = OVER_SCROLL_IF_CONTENT_SCROLLS isFillViewport = false clipToPadding = false - setPadding(0, dp(8), dp(4), dp(6)) - // WRAP_CONTENT is required — MATCH_PARENT (default) clips chips instead of scrolling. + setPadding(0, dp(8), dp(8), dp(4)) addView( actionsRow, 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 { diff --git a/app/src/main/res/drawable/ic_chip_todo.xml b/app/src/main/res/drawable/ic_chip_todo.xml index a129268..d8d7283 100644 --- a/app/src/main/res/drawable/ic_chip_todo.xml +++ b/app/src/main/res/drawable/ic_chip_todo.xml @@ -1,4 +1,5 @@ + + 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" /> + diff --git a/scripts/smoke-circle.sh b/scripts/smoke-circle.sh index 77f4aa9..3612108 100755 --- a/scripts/smoke-circle.sh +++ b/scripts/smoke-circle.sh @@ -147,30 +147,18 @@ echo "$logs" | grep -qE 'extract:.*chars=[1-9]' || die "case2: no text extracted pass "email region extract" "$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 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" - sleep 0.35 + sleep 0.3 if "$ADB" logcat -d | grep -q 'action kind=COPY'; then copied=1 break 2 fi 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" "$ADB" logcat -d | grep -q 'action result ok=true' || die "Copy failed" pass "Copy chip"