app/src/main/kotlin/space/subread/overlay/PlayerProvider.kt (5990 bytes)
1 package space.subread.overlay 2 3 import android.content.ComponentName 4 import android.content.ContentProvider 5 import android.content.ContentValues 6 import android.database.Cursor 7 import android.database.MatrixCursor 8 import android.media.session.MediaController 9 import android.media.session.MediaSessionManager 10 import android.media.session.PlaybackState 11 import android.net.Uri 12 import android.os.Bundle 13 import android.os.SystemClock 14 import space.subread.overlay.core.PlayClock 15 16 /** 17 * Gives the position of the media player to other apps, and takes play, pause and seek from them. 18 * 19 * A reader app, for example KOReader with the SubRead plugin, has no notification access, so it 20 * cannot see the media session of the player. This app has that access, so it answers for them. 21 * 22 * `query` returns one row with one column, `state`. Its value is one line: 23 * 24 * playing=1;position=96153;speed=1.0;package=de.ph1b.audiobook 25 * 26 * The position is in milliseconds, computed for the moment of the query. A problem is one line 27 * `error=<reason>`: `no_notification_access` or `no_player`. 28 * 29 * `call` takes the method `play`, `pause` or `seek` (the argument is the position in 30 * milliseconds), and returns the state line in the bundle key `state`. 31 * 32 * A caption app, one that makes captions from live audio, sends its lines with the method `line` 33 * (the argument is the text, the extra `partial` is true while the sentence goes on) and `end` 34 * when it stops. The panel shows the newest line, in place of the subtitle file, until `end`. 35 * The bundle key `live` answers `ok`, or the reason the panel cannot show the line: 36 * `no_notification_access`, `no_overlay_permission` or `panel_hidden`. 37 * 38 * The provider is open to each app. Each app can already send the media keys to the player, so 39 * this gives no new control over the device. A line is shown, not kept or sent. 40 */ 41 class PlayerProvider : ContentProvider() { 42 43 private val sessions by lazy { context!!.getSystemService(MediaSessionManager::class.java) } 44 private val listener by lazy { ComponentName(context!!, MediaListener::class.java) } 45 46 override fun onCreate(): Boolean = true 47 48 override fun query( 49 uri: Uri, 50 projection: Array<String>?, 51 selection: String?, 52 selectionArgs: Array<String>?, 53 sortOrder: String?, 54 ): Cursor = MatrixCursor(arrayOf(COLUMN_STATE)).apply { addRow(arrayOf(stateLine())) } 55 56 override fun call(method: String, arg: String?, extras: Bundle?): Bundle { 57 if (method == METHOD_LINE || method == METHOD_END) { 58 val listener = MediaListener.instance 59 val answer = when { 60 listener == null -> ERROR_NO_ACCESS 61 method == METHOD_END -> listener.endLive() 62 else -> listener.liveLine(arg.orEmpty(), extras?.getBoolean(EXTRA_PARTIAL) == true) 63 } 64 return Bundle().apply { putString(KEY_LIVE, answer) } 65 } 66 val player = runCatching { player() }.getOrNull() 67 val controls = player?.transportControls 68 when (method) { 69 METHOD_PLAY -> controls?.play() 70 METHOD_PAUSE -> controls?.pause() 71 METHOD_SEEK -> arg?.toLongOrNull()?.let { controls?.seekTo(it) } 72 } 73 return Bundle().apply { putString(COLUMN_STATE, stateLine()) } 74 } 75 76 /** The session that plays, else the first active one. Null when there is none. */ 77 private fun player(): MediaController? { 78 val active = sessions.getActiveSessions(listener) 79 return active.firstOrNull { it.playbackState?.state == PlaybackState.STATE_PLAYING } ?: active.firstOrNull() 80 } 81 82 private fun stateLine(): String { 83 val player = try { 84 player() 85 } catch (e: SecurityException) { 86 return "error=$ERROR_NO_ACCESS" 87 } ?: return "error=$ERROR_NO_PLAYER" 88 val state = player.playbackState ?: return "error=$ERROR_NO_PLAYER" 89 return stateLine(state, player.packageName, SystemClock.elapsedRealtime()) 90 } 91 92 override fun getType(uri: Uri): String = "vnd.android.cursor.item/vnd.space.subread.overlay.state" 93 94 override fun insert(uri: Uri, values: ContentValues?): Uri? = null 95 96 override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int = 0 97 98 override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0 99 100 companion object { 101 // The debug build has its own authority, so it installs next to the release. 102 const val AUTHORITY = BuildConfig.APPLICATION_ID + ".player" 103 const val COLUMN_STATE = "state" 104 const val METHOD_PLAY = "play" 105 const val METHOD_PAUSE = "pause" 106 const val METHOD_SEEK = "seek" 107 const val METHOD_LINE = "line" 108 const val METHOD_END = "end" 109 const val EXTRA_PARTIAL = "partial" 110 const val KEY_LIVE = "live" 111 const val LIVE_OK = "ok" 112 const val ERROR_NO_ACCESS = "no_notification_access" 113 const val ERROR_NO_PLAYER = "no_player" 114 const val ERROR_NO_OVERLAY = "no_overlay_permission" 115 const val ERROR_PANEL_HIDDEN = "panel_hidden" 116 117 /** The state line for a report of the player, at [nowMs] on the clock of the device. */ 118 fun stateLine(state: PlaybackState, packageName: String, nowMs: Long): String { 119 val playing = state.state == PlaybackState.STATE_PLAYING 120 val speed = state.playbackSpeed.takeIf { it > 0f } ?: 1f 121 // A report without a position: the same as the panel, the position is unknown. 122 val position = if (state.position >= 0) { 123 val reportedAt = state.lastPositionUpdateTime.takeIf { it > 0 } ?: nowMs 124 PlayClock(state.position, reportedAt, speed, playing).positionAt(nowMs) 125 } else { 126 -1 127 } 128 return "playing=${if (playing) 1 else 0};position=$position;speed=$speed;package=$packageName" 129 } 130 } 131 }