Recently Written · git

subrep-android

Subrep on Android: live captions of the sound of the phone, made on the phone by Whisper, shared by link.

git clone https://github.com/equwal/subrep-android

Log | Files | Refs


app/src/main/kotlin/com/honjimaku/subrep/Overlay.kt (3116 bytes)

1 package com.honjimaku.subrep
2 
3 import android.content.Context
4 import android.content.Intent
5 import android.net.Uri
6 import android.os.Bundle
7 import android.os.Handler
8 import android.os.SystemClock
9 
10 /**
11  * SubRead Overlay: the panel over the player. It takes a line with the method `line` of its
12  * content provider, and answers in the key `live` whether it shows it.
13  */
14 object Overlay {
15 
16     const val PACKAGE = "space.subread.overlay"
17     const val INSTALL = "https://github.com/equwal/subread-overlay/releases/latest"
18     private const val AUTHORITY = "space.subread.overlay.player"
19     private val uri: Uri = Uri.parse("content://$AUTHORITY")
20 
21     const val OK = "ok"
22     const val NOT_INSTALLED = "not_installed"
23 
24     /** The overlay's answer for a version that has no `line` method. */
25     const val TOO_OLD = "too_old"
26 
27     fun installed(context: Context): Boolean = context.packageManager.resolveContentProvider(AUTHORITY, 0) != null
28 
29     fun launch(context: Context): Intent? = context.packageManager.getLaunchIntentForPackage(PACKAGE)
30 
31     fun line(context: Context, text: String, partial: Boolean): String =
32         call(context, "line", text, Bundle().apply { putBoolean("partial", partial) })
33 
34     fun end(context: Context): String = call(context, "end", null, null)
35 
36     private fun call(context: Context, method: String, arg: String?, extras: Bundle?): String {
37         val answer = try {
38             context.contentResolver.call(uri, method, arg, extras)
39         } catch (e: Exception) {
40             return NOT_INSTALLED
41         } ?: return NOT_INSTALLED
42         return answer.getString("live") ?: TOO_OLD
43     }
44 }
45 
46 /**
47  * Sends the captions to the overlay from its own thread, because a provider call waits for
48  * the other app. Partial lines go at most every [PARTIAL_GAP_MS]: the overlay draws each line,
49  * and an e-ink screen cannot draw five times a second.
50  */
51 class OverlayFeed(private val context: Context, private val handler: Handler) {
52 
53     private var pendingPartial: String? = null
54     private var lastAt = 0L
55     private val flush = Runnable {
56         pendingPartial?.let { send(it, partial = true) }
57         pendingPartial = null
58     }
59 
60     fun caption(caption: Caption) {
61         handler.post {
62             when (caption.type) {
63                 "final" -> {
64                     handler.removeCallbacks(flush)
65                     pendingPartial = null
66                     send(caption.text, partial = false)
67                 }
68                 "partial" -> {
69                     pendingPartial = caption.text
70                     handler.removeCallbacks(flush)
71                     handler.postDelayed(flush, (PARTIAL_GAP_MS - (SystemClock.elapsedRealtime() - lastAt)).coerceAtLeast(0))
72                 }
73             }
74         }
75     }
76 
77     fun end() {
78         handler.removeCallbacks(flush)
79         handler.post { Overlay.end(context) }
80     }
81 
82     private fun send(text: String, partial: Boolean) {
83         lastAt = SystemClock.elapsedRealtime()
84         Feed.overlay(Overlay.line(context, text, partial))
85     }
86 
87     private companion object {
88         const val PARTIAL_GAP_MS = 700L
89     }
90 }