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/channel/AiKeyActivity.kt (4123 bytes)

1 package dev.equwal.assistkey.channel
2 
3 import android.app.Activity
4 import android.content.Context
5 import android.os.Bundle
6 import android.os.Handler
7 import android.os.Looper
8 import dev.equwal.assistkey.engine.KeyFilterService
9 import dev.equwal.assistkey.license.License
10 import dev.equwal.assistkey.model.ActionKind
11 import dev.equwal.assistkey.model.ActionSpec
12 import dev.equwal.assistkey.model.GestureType
13 import dev.equwal.assistkey.model.HwKey
14 import dev.equwal.assistkey.model.Presets
15 import dev.equwal.assistkey.model.Trigger
16 import dev.equwal.assistkey.route.ActionRouter
17 import dev.equwal.assistkey.route.ServiceHolder
18 import dev.equwal.assistkey.store.Store
19 
20 /**
21  * Where the AI key lands once the firmware hook points here.
22  *
23  * Measured on firmware 1.5.6: with the stock hook the firmware opens its AI
24  * screen on every press whether or not an app consumed the key, so a binding
25  * made through the key filter always fires on top of that screen. With the
26  * hook set to anything else the firmware launches that component instead and
27  * the filter never sees the key. Pointing it here is therefore the only way to
28  * own the AI key cleanly:
29  *
30  *   adb shell settings put system CustomAiKey \
31  *       dev.equwal.assistkey/dev.equwal.assistkey.channel.AiKeyActivity
32  *
33  * Each press arrives as one launch, which is enough to count taps but says
34  * nothing about release - so taps, and "hold Power, then the AI key", but no
35  * press-and-hold and no combinations with the volume keys.
36  */
37 class AiKeyActivity : Activity() {
38 
39     override fun onCreate(savedInstanceState: Bundle?) {
40         super.onCreate(savedInstanceState)
41         val svc = ServiceHolder.service as? KeyFilterService
42 
43         when {
44             // Inside an AI screen the key means "back to what I was doing".
45             svc != null && svc.inAiScreen && License.active(this) &&
46                 dev.equwal.assistkey.device.Device.aiKeyReturns(this) -> svc.returnFromAiScreen()
47             // Ahead of the licence check: the navigation lifeline lives here.
48             svc != null && svc.takePowerCombo(HwKey.AI) -> Unit
49             // Locked means the key behaves as the firmware intended.
50             !License.active(this) -> ActionRouter.run(this, STOCK)
51             else -> AiKeyTaps.press(this)
52         }
53 
54         finish()
55         @Suppress("DEPRECATION")
56         overridePendingTransition(0, 0)
57     }
58 
59     companion object {
60         val STOCK = ActionSpec(ActionKind.LAUNCH_COMPONENT, Presets.STOCK_AI_KEY)
61     }
62 }
63 
64 /** Counts launches into taps, with the same rules the gesture engine uses. */
65 object AiKeyTaps {
66 
67     private val keys = setOf(HwKey.AI)
68     private val handler = Handler(Looper.getMainLooper())
69     private var count = 0
70     private var pending: Runnable? = null
71 
72     fun press(activity: Activity) {
73         val app = activity.applicationContext
74         val max = Store.bindings(app).maxTaps(keys)
75         if (max == 0) {
76             // Nothing bound: be the stock key, and do it while still in front,
77             // because a background launch would be refused.
78             ActionRouter.run(activity, AiKeyActivity.STOCK)
79             return
80         }
81         pending?.let { handler.removeCallbacks(it) }
82         count++
83         val n = count
84         // The highest bound count cannot grow into anything, so it fires now;
85         // anything lower has to wait out the multi-tap window.
86         val delay = if (n >= max) SETTLE_MS else Store.timing(app).multiTapMs.toLong()
87         val r = Runnable { fire(app, n) }
88         pending = r
89         handler.postDelayed(r, delay)
90     }
91 
92     private fun fire(app: Context, n: Int) {
93         count = 0
94         pending = null
95         val spec = Store.bindings(app)[Trigger(keys, GestureType.TAP, n)]
96             ?: if (n == 1) AiKeyActivity.STOCK else return
97         // The accessibility service may start activities from the background;
98         // a plain application context may not.
99         ActionRouter.run(ServiceHolder.service ?: app, spec)
100     }
101 
102     /** Long enough for our own invisible window to be gone before Back or Home lands. */
103     private const val SETTLE_MS = 150L
104 }