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 }