Recently Written · git

subread-anki

One tap makes an Anki card on Android: word, definition, sentence, screenshot, audio. Works with any app, with SubRead Dictionary and SubRead Overlay.

git clone https://github.com/equwal/subread-anki

Log | Files | Refs


app/src/main/kotlin/space/subread/anki/OverlayClient.kt (4397 bytes)

1 package space.subread.anki
2 
3 import android.content.Context
4 import android.database.Cursor
5 import android.os.SystemClock
6 import androidx.core.net.toUri
7 import space.subread.anki.core.PlayerReport
8 
9 /**
10  * Asks SubRead Overlay for the player and the subtitle line of now, through its content
11  * provider `content://space.subread.overlay.player`.
12  *
13  * `/state` is in every version of the overlay. `/line` came with the Anki support: a version
14  * without it answers the state row for every path, and then the line is unknown.
15  */
16 object OverlayClient {
17 
18     const val PACKAGE = "space.subread.overlay"
19 
20     /** The release authority, then the one of a debug build, which installs next to the release. */
21     val AUTHORITIES = listOf("space.subread.overlay.player", "space.subread.overlay.debug.player")
22 
23     /** The authority of the installed overlay, or null without one. */
24     fun authority(context: Context): String? =
25         AUTHORITIES.firstOrNull { context.packageManager.resolveContentProvider(it, 0) != null }
26 
27     /** The subtitle line of now. The times are on the clock of the subtitle file. */
28     data class Line(
29         val index: Int,
30         val startMs: Long,
31         val endMs: Long,
32         val text: String,
33         val before: String?,
34         val after: String?,
35         /** The shift the user set: a time of the subtitle file is `offsetMs` later than the player. */
36         val offsetMs: Long,
37     )
38 
39     /** What the overlay knows now. [report] is on the clock of `SystemClock.elapsedRealtimeNanos()`. */
40     data class Now(val report: PlayerReport?, val player: String?, val line: Line?)
41 
42     fun installed(context: Context): Boolean = authority(context) != null
43 
44     /**
45      * The line of now and the player. Null when the overlay is not installed or does not answer.
46      * With a release and a debug overlay side by side, the one that shows a line wins, else the
47      * one that sees a player.
48      */
49     fun now(context: Context): Now? {
50         val answers = AUTHORITIES.mapNotNull { authority ->
51             if (context.packageManager.resolveContentProvider(authority, 0) == null) null else now(context, authority)
52         }
53         return answers.firstOrNull { it.line != null } ?: answers.firstOrNull { it.report != null } ?: answers.firstOrNull()
54     }
55 
56     private fun now(context: Context, authority: String): Now? {
57         val cursor = runCatching {
58             context.contentResolver.query("content://$authority/line".toUri(), null, null, null, null)
59         }.getOrNull() ?: return null
60         cursor.use { c ->
61             if (!c.moveToFirst()) return null
62             val state = c.text("state")
63             val player = PlayerReport.overlayPackage(state)
64             val report = report(c, state)
65             val text = c.text("text")
66             val line = if (text == null) null else Line(
67                 index = c.long("index")?.toInt() ?: -1,
68                 startMs = c.long("start") ?: 0,
69                 endMs = c.long("end") ?: 0,
70                 text = text,
71                 before = c.text("before"),
72                 after = c.text("after"),
73                 offsetMs = c.long("offset") ?: 0,
74             )
75             return Now(report, player, line)
76         }
77     }
78 
79     /**
80      * The report of the player. The line row gives the raw report: the position at the moment
81      * the player reported it. A pause reported a minute ago still maps its media times to the
82      * right wall-clock times. The state row alone gives the position of now.
83      */
84     private fun report(c: Cursor, state: String?): PlayerReport? {
85         val position = c.long("reported_position")
86         val at = c.long("reported_at")
87         if (position != null && position >= 0 && at != null) {
88             val speed = c.text("speed")?.toDoubleOrNull()?.takeIf { it > 0 } ?: 1.0
89             val playing = c.long("playing") == 1L
90             return PlayerReport(position, at * 1_000_000L, speed, playing)
91         }
92         return PlayerReport.parseOverlayState(state, SystemClock.elapsedRealtimeNanos())?.takeIf { it.positionMs >= 0 }
93     }
94 
95     private fun Cursor.text(column: String): String? {
96         val i = getColumnIndex(column)
97         return if (i < 0 || isNull(i)) null else getString(i)
98     }
99 
100     private fun Cursor.long(column: String): Long? {
101         val i = getColumnIndex(column)
102         return if (i < 0 || isNull(i)) null else getLong(i)
103     }
104 }