Recently Written · git

assistkey

Hardware key remapper for the Viwoods AiPaper Reader: AI key, volume keys and power, with multi-tap, hold and chords

git clone https://github.com/equwal/assistkey

Log | Files | Refs


app/src/main/java/dev/equwal/assistkey/engine/ScreenButton.kt (4544 bytes)

1 package dev.equwal.assistkey.engine
2 
3 import android.accessibilityservice.AccessibilityService
4 import android.content.Context
5 import android.graphics.Canvas
6 import android.graphics.Color
7 import android.graphics.Paint
8 import android.graphics.PixelFormat
9 import android.view.Gravity
10 import android.view.MotionEvent
11 import android.view.View
12 import android.view.ViewConfiguration
13 import android.view.WindowManager
14 import kotlin.math.abs
15 
16 /**
17  * A small round button that floats above every app.
18  *
19  * The key filter draws it in an accessibility overlay window, which needs no
20  * permission of its own. It is on the screen only while a binding for it
21  * exists, so a user who does not want it never sees it. A tap runs the
22  * binding. A drag moves the button, and the place is kept.
23  *
24  * Android has its own accessibility button. It is not used: Android shows that
25  * button to every user as soon as a service asks for it, bound or not.
26  *
27  * Drawn for e-ink: a white disc with a black ring, no shadow, no animation.
28  */
29 class ScreenButton(private val service: AccessibilityService, private val onTap: () -> Unit) {
30 
31     private val wm = service.getSystemService(WindowManager::class.java)
32     private val prefs = service.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
33     private val d = service.resources.displayMetrics.density
34     private val size = (48 * d).toInt()
35     private var view: View? = null
36 
37     private val params = WindowManager.LayoutParams(
38         size, size,
39         WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
40         WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
41         PixelFormat.TRANSLUCENT
42     ).apply { gravity = Gravity.TOP or Gravity.START }
43 
44     val showing: Boolean get() = view != null
45 
46     fun show() {
47         if (view != null) return
48         val screen = service.resources.displayMetrics
49         params.x = prefs.getInt(K_X, screen.widthPixels - size - (12 * d).toInt())
50         params.y = prefs.getInt(K_Y, screen.heightPixels * 2 / 3)
51         val v = Disc(service)
52         runCatching { wm.addView(v, params) }.onSuccess { view = v }
53     }
54 
55     fun hide() {
56         view?.let { runCatching { wm.removeView(it) } }
57         view = null
58     }
59 
60     private inner class Disc(c: Context) : View(c) {
61         private val ring = Paint(Paint.ANTI_ALIAS_FLAG).apply {
62             style = Paint.Style.STROKE; strokeWidth = 3 * d; color = Color.BLACK
63         }
64         private val paper = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL; color = Color.WHITE }
65         private val dot = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL; color = Color.BLACK }
66         private val slop = ViewConfiguration.get(c).scaledTouchSlop
67         private var downX = 0f
68         private var downY = 0f
69         private var startX = 0
70         private var startY = 0
71         private var dragged = false
72 
73         override fun onDraw(canvas: Canvas) {
74             val r = width / 2f
75             canvas.drawCircle(r, r, r - 2 * d, paper)
76             canvas.drawCircle(r, r, r - 3 * d, ring)
77             canvas.drawCircle(r, r, r * 0.30f, dot)
78         }
79 
80         override fun onTouchEvent(e: MotionEvent): Boolean {
81             when (e.actionMasked) {
82                 MotionEvent.ACTION_DOWN -> {
83                     downX = e.rawX; downY = e.rawY
84                     startX = params.x; startY = params.y
85                     dragged = false
86                 }
87                 MotionEvent.ACTION_MOVE -> {
88                     val dx = e.rawX - downX
89                     val dy = e.rawY - downY
90                     if (!dragged && (abs(dx) > slop || abs(dy) > slop)) dragged = true
91                     if (dragged) {
92                         params.x = startX + dx.toInt()
93                         params.y = startY + dy.toInt()
94                         runCatching { wm.updateViewLayout(this, params) }
95                     }
96                 }
97                 MotionEvent.ACTION_UP -> {
98                     if (dragged) {
99                         prefs.edit().putInt(K_X, params.x).putInt(K_Y, params.y).apply()
100                     } else {
101                         performClick()
102                     }
103                 }
104             }
105             return true
106         }
107 
108         override fun performClick(): Boolean {
109             super.performClick()
110             onTap()
111             return true
112         }
113     }
114 
115     private companion object {
116         const val PREFS = "assistkey_screen_button"
117         const val K_X = "x"
118         const val K_Y = "y"
119     }
120 }