feat(app-glasses): жесты RayNeo SDK (TempleAction) + отключение звуков трекпада

Перенос из pair-view (проверено на железе): RayNeoARSDK V0.1.3 (aar в
libs/, MercurySDK.init в GlassesApp), MainActivity → BaseEventActivity,
templeActionViewModel.state → команды (Click=пауза/плей, SlideForward/
Backward=±30с), dispatchTouchEvent→onTouchEvent + toggleSoundEffect(0..3,
false) — системные щелчки трекпада отключены.

Фиксы по ходу (железо):
- applyMediaCommand(msg, onDone) — sendPlaybackPositionNow после seekTo
  (иначе телефон получал позицию ДО seek)
- догонка seekTo через 1с на телефоне (первый seekTo в ogg неточен)
- playbackPosition: isPlaying → playWhenReady — ложное playing=false при
  буферизации после seekTo паузило звук на телефоне (~5с «пропажи»);
  pauseIfPlaying тоже по playWhenReady

Тесты: glasses 59+5, phone 71. Железо: клик пауза/плей ✓, свайпы ±30с ✓,
звук при seek не пропадает ✓ (подтверждено пользователем)
This commit is contained in:
Hermes Agent
2026-08-19 23:38:45 +03:00
parent 654a5f9e26
commit 345e7b74fa
7 changed files with 196 additions and 9 deletions
@@ -0,0 +1,67 @@
package pw.binom.viewmate.glasses.gestures
import android.view.MotionEvent
import com.rayneo.arsdk.android.touch.FlingArgs
import com.rayneo.arsdk.android.touch.TempleAction
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import pw.binom.viewmate.core.MediaCommand
/**
* Маппинг жестов SDK → команды просмотра (чистая логика, без Android).
* FlingArgs требует не-null MotionEvent; в JVM-тесте реальный экземпляр
* MotionEvent получаем через sun.misc.Unsafe.allocateInstance (без вызова
* конструктора и методов фреймворка — в unit-тестах они "not mocked").
* Поля args в templeActionToCommand не читаются — важен только тип жеста.
*/
class GestureCommandsTest {
private val flingArgs = FlingArgs(fakeMotionEvent(), fakeMotionEvent(), 0f, 0f)
@Test
fun clickMapsToToggle() {
val cmd = templeActionToCommand(TempleAction.Click())
assertEquals(MediaCommand.TOGGLE, cmd?.command)
assertEquals(0L, cmd?.seekMs)
}
@Test
fun slideForwardMapsToSeekForward() {
val cmd = templeActionToCommand(TempleAction.SlideForward(flingArgs))
assertEquals(MediaCommand.SEEK_FORWARD, cmd?.command)
assertEquals(30_000L, cmd?.seekMs)
}
@Test
fun slideBackwardMapsToSeekBackward() {
val cmd = templeActionToCommand(TempleAction.SlideBackward(flingArgs))
assertEquals(MediaCommand.SEEK_BACKWARD, cmd?.command)
assertEquals(30_000L, cmd?.seekMs)
}
@Test
fun customSeekStepIsUsed() {
val cmd = templeActionToCommand(TempleAction.SlideForward(flingArgs), seekStepMs = 60_000)
assertEquals(60_000L, cmd?.seekMs)
}
@Test
fun reservedGesturesMapToNull() {
assertNull(templeActionToCommand(TempleAction.DoubleClick()))
assertNull(templeActionToCommand(TempleAction.LongClick()))
assertNull(templeActionToCommand(TempleAction.TripleClick()))
assertNull(templeActionToCommand(TempleAction.MoveUp(false)))
assertNull(templeActionToCommand(TempleAction.ActionUp()))
assertNull(templeActionToCommand(TempleAction.Idle))
}
private fun fakeMotionEvent(): MotionEvent {
val unsafeClass = Class.forName("sun.misc.Unsafe")
val theUnsafe = unsafeClass.getDeclaredField("theUnsafe")
theUnsafe.isAccessible = true
val unsafe = theUnsafe.get(null)
val allocate = unsafeClass.getMethod("allocateInstance", Class::class.java)
return allocate.invoke(unsafe, MotionEvent::class.java) as MotionEvent
}
}