Recently Written · git

subread-overlay

Shows the lines of an .srt over any Android media player, in time with it. Tap a word to look it up.

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

Log | Files | Refs


app/src/androidTest/kotlin/space/subread/overlay/PlayerProviderTest.kt (6658 bytes)

1 package space.subread.overlay
2 
3 import android.media.session.MediaSession
4 import android.media.session.PlaybackState
5 import android.net.Uri
6 import android.os.SystemClock
7 import androidx.core.net.toUri
8 import androidx.test.ext.junit.runners.AndroidJUnit4
9 import androidx.test.platform.app.InstrumentationRegistry
10 import org.junit.Assert.assertEquals
11 import org.junit.Assert.assertTrue
12 import org.junit.Assume.assumeTrue
13 import org.junit.Test
14 import org.junit.runner.RunWith
15 import java.io.File
16 
17 /**
18  * The provider that a reader app asks for the position. The test app plays the part of the
19  * player with its own media session. The device must give this app notification access, else
20  * the test is skipped.
21  */
22 @RunWith(AndroidJUnit4::class)
23 class PlayerProviderTest {
24 
25     private val context = InstrumentationRegistry.getInstrumentation().targetContext
26     private val uri = "content://${PlayerProvider.AUTHORITY}/state".toUri()
27     private val lineUri = "content://${PlayerProvider.AUTHORITY}/${PlayerProvider.PATH_LINE}".toUri()
28 
29     private fun state(state: Int, position: Long, speed: Float = 1f) = PlaybackState.Builder()
30         .setState(state, position, speed, SystemClock.elapsedRealtime())
31         .setActions(PlaybackState.ACTION_PLAY or PlaybackState.ACTION_PAUSE or PlaybackState.ACTION_SEEK_TO)
32         .build()
33 
34     private fun read(): Map<String, String> {
35         val cursor = context.contentResolver.query(uri, null, null, null, null)!!
36         cursor.use {
37             assertTrue(it.moveToFirst())
38             val line = it.getString(it.getColumnIndexOrThrow(PlayerProvider.COLUMN_STATE))
39             return line.split(';').associate { part -> part.substringBefore('=') to part.substringAfter('=') }
40         }
41     }
42 
43     @Test
44     fun theStateLineHasThePositionOfNow() {
45         val paused = PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 1_000, 1f, 5_000).build()
46         assertEquals(
47             "playing=0;position=1000;speed=1.0;package=p",
48             PlayerProvider.stateLine(paused, "p", 9_000),
49         )
50         val playing = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 1_000, 2f, 5_000).build()
51         assertEquals(
52             "playing=1;position=9000;speed=2.0;package=p",
53             PlayerProvider.stateLine(playing, "p", 9_000),
54         )
55         val unknown = PlaybackState.Builder()
56             .setState(PlaybackState.STATE_PLAYING, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1f, 0).build()
57         assertEquals("playing=1;position=-1;speed=1.0;package=p", PlayerProvider.stateLine(unknown, "p", 9_000))
58     }
59 
60     @Test
61     fun aReaderAppReadsThePlayerAndControlsIt() {
62         assumeTrue("notification access is not given to this app", MediaListener.isAllowed(context))
63         val session = MediaSession(context, "test player")
64         val seeks = mutableListOf<Long>()
65         var plays = 0
66         var pauses = 0
67         session.setCallback(object : MediaSession.Callback() {
68             override fun onPlay() { plays++ }
69             override fun onPause() { pauses++ }
70             override fun onSeekTo(pos: Long) { seeks += pos }
71         })
72         try {
73             session.setPlaybackState(state(PlaybackState.STATE_PLAYING, 96_000, speed = 1.5f))
74             session.isActive = true
75 
76             val seen = read()
77             assertEquals("1", seen["playing"])
78             assertEquals("1.5", seen["speed"])
79             assertEquals(context.packageName, seen["package"])
80             val position = seen["position"]!!.toLong()
81             assertTrue("position $position is not near 96000", position in 96_000..97_500)
82 
83             val resolver = context.contentResolver
84             resolver.call(uri, PlayerProvider.METHOD_PAUSE, null, null)
85             resolver.call(uri, PlayerProvider.METHOD_PLAY, null, null)
86             val answer = resolver.call(uri, PlayerProvider.METHOD_SEEK, "12345", null)!!
87             val until = SystemClock.elapsedRealtime() + 5_000
88             while ((plays < 1 || pauses < 1 || seeks.isEmpty()) && SystemClock.elapsedRealtime() < until) Thread.sleep(20)
89             assertEquals(1, plays)
90             assertEquals(1, pauses)
91             assertEquals(listOf(12_345L), seeks)
92             assertTrue(answer.getString(PlayerProvider.COLUMN_STATE)!!.startsWith("playing="))
93         } finally {
94             session.release()
95         }
96     }
97 
98     /** A flash card app asks for the line of a position. No player and no notification access are needed for that. */
99     @Test
100     fun aFlashCardAppReadsTheLineOfAPosition() {
101         val store = Store(context)
102         val before = store.subtitles
103         val beforeOffset = store.offsetMs
104         val file = File(context.cacheDir, "line-test.srt").apply {
105             writeText("1\n00:01:30,000 --> 00:01:33,000\nFirst line.\n\n2\n00:01:36,000 --> 00:01:39,500\nSecond line.\n\n")
106         }
107         try {
108             store.subtitles = Uri.fromFile(file)
109             store.offsetMs = 1_000
110             // The player is at 95 s; the file is shifted 1 s later: the line at 96 s of the file.
111             val at95 = lineUri.buildUpon().appendQueryParameter(PlayerProvider.PARAM_POSITION, "95000").build()
112             context.contentResolver.query(at95, null, null, null, null)!!.use {
113                 assertTrue(it.moveToFirst())
114                 assertEquals("Second line.", it.getString(it.getColumnIndexOrThrow(PlayerProvider.COLUMN_TEXT)))
115                 assertEquals(96_000L, it.getLong(it.getColumnIndexOrThrow(PlayerProvider.COLUMN_START)))
116                 assertEquals(99_500L, it.getLong(it.getColumnIndexOrThrow(PlayerProvider.COLUMN_END)))
117                 assertEquals(1L, it.getLong(it.getColumnIndexOrThrow(PlayerProvider.COLUMN_INDEX)))
118                 assertEquals(1_000L, it.getLong(it.getColumnIndexOrThrow(PlayerProvider.COLUMN_OFFSET)))
119                 assertEquals("First line.", it.getString(it.getColumnIndexOrThrow(PlayerProvider.COLUMN_BEFORE)))
120                 assertTrue(it.isNull(it.getColumnIndexOrThrow(PlayerProvider.COLUMN_AFTER)))
121             }
122             // Before the first line: no text.
123             val at10 = lineUri.buildUpon().appendQueryParameter(PlayerProvider.PARAM_POSITION, "10000").build()
124             context.contentResolver.query(at10, null, null, null, null)!!.use {
125                 assertTrue(it.moveToFirst())
126                 assertEquals(-1L, it.getLong(it.getColumnIndexOrThrow(PlayerProvider.COLUMN_INDEX)))
127                 assertTrue(it.isNull(it.getColumnIndexOrThrow(PlayerProvider.COLUMN_TEXT)))
128             }
129         } finally {
130             store.subtitles = before
131             store.offsetMs = beforeOffset
132             file.delete()
133         }
134     }
135 }