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 }