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 }