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 }