Skip to content

Commit 96fa5a6

Browse files
authored
fix: Cache errors when rapidly switching sources (#68)
1 parent 5493827 commit 96fa5a6

File tree

7 files changed

+79
-45
lines changed

7 files changed

+79
-45
lines changed

library/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ android {
1616

1717
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1818
consumerProguardFiles "consumer-rules.pro"
19+
20+
multiDexEnabled true
1921
}
2022

2123
buildTypes {

library/src/androidTest/java/com/mux/player/CacheDatastoreInstrumentationTests.kt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.mux.player.internal.Constants
88
import com.mux.player.internal.cache.CacheDatastore
99
import com.mux.player.internal.cache.FileRecord
1010
import com.mux.player.internal.cache.filesDirNoBackupCompat
11+
import com.mux.player.internal.createLogcatLogger
1112
import org.junit.Assert
1213
import org.junit.Before
1314
import org.junit.Test
@@ -56,7 +57,7 @@ class CacheDatastoreInstrumentationTests {
5657

5758
@Test
5859
fun testBasicInitialization() {
59-
val datastore = CacheDatastore(appContext)
60+
val datastore = CacheDatastore(appContext, logger = createLogcatLogger())
6061
datastore.use { it.open() }
6162

6263
Assert.assertTrue(
@@ -80,7 +81,7 @@ class CacheDatastoreInstrumentationTests {
8081

8182
@Test
8283
fun testCreateTempDownloadFile() {
83-
val datastore = CacheDatastore(appContext)
84+
val datastore = CacheDatastore(appContext, logger = createLogcatLogger())
8485
datastore.use {
8586
it.open()
8687

@@ -102,7 +103,7 @@ class CacheDatastoreInstrumentationTests {
102103
val oldFileData = "old data".toByteArray(Charsets.UTF_8)
103104
val newFileData = "new data".toByteArray(Charsets.UTF_8)
104105

105-
val datastore = CacheDatastore(appContext)
106+
val datastore = CacheDatastore(appContext, logger = createLogcatLogger())
106107
datastore.open()
107108
datastore.use {
108109
// Write one file...
@@ -132,7 +133,7 @@ class CacheDatastoreInstrumentationTests {
132133

133134
@Test
134135
fun testWriteRecordReplacesOnKey() {
135-
val datastore = CacheDatastore(appContext)
136+
val datastore = CacheDatastore(appContext, logger = createLogcatLogger())
136137
datastore.use {
137138
datastore.open()
138139

@@ -183,7 +184,7 @@ class CacheDatastoreInstrumentationTests {
183184
@Test
184185
fun testReadRecord() {
185186
fun testTheCase(url: String) {
186-
CacheDatastore(appContext).use { datastore ->
187+
CacheDatastore(appContext, logger = createLogcatLogger()).use { datastore ->
187188
datastore.open()
188189

189190
val originalRecord = FileRecord(
@@ -216,7 +217,7 @@ class CacheDatastoreInstrumentationTests {
216217
@Test
217218
fun testReadLeastRecentFiles() {
218219
val maxCacheSize = 5L
219-
CacheDatastore(appContext, maxDiskSize = maxCacheSize).use { datastore ->
220+
CacheDatastore(appContext, maxDiskSize = maxCacheSize, createLogcatLogger()).use { datastore ->
220221
datastore.open()
221222
// For this test, size "units" are like one digit.
222223
// time "units" start in the 3-digit range and tick at ~10 units per call to fakeNow()
@@ -279,7 +280,7 @@ class CacheDatastoreInstrumentationTests {
279280
val maxCacheSize = 5500L
280281
val dummyFileSize = 1000L
281282

282-
CacheDatastore(appContext, maxDiskSize = maxCacheSize).use { datastore ->
283+
CacheDatastore(appContext, maxDiskSize = maxCacheSize, createLogcatLogger()).use { datastore ->
283284
datastore.open()
284285
// time "units" start in the 3-digit range and tick at ~10 units per call to fakeNow()
285286
var fakeLastAccess = 200L // increment by some amount when you need to

library/src/main/java/com/mux/player/MuxPlayer.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,10 @@ class MuxPlayer private constructor(
138138
private var customerData: CustomerData = CustomerData()
139139
private var exoPlayerBinding: ExoPlayerBinding? = null
140140
private var network: INetworkRequest? = null
141-
private var muxPlayerCache: MuxPlayerCache = MuxPlayerCache.create(context.applicationContext)
141+
private var muxPlayerCache: MuxPlayerCache = MuxPlayerCache.create(
142+
context.applicationContext,
143+
logger = logger ?: createNoLogger()
144+
)
142145

143146
constructor(context: Context) : this(context, ExoPlayer.Builder(context))
144147

library/src/main/java/com/mux/player/internal/cache/CacheDatastore.kt

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import android.database.sqlite.SQLiteDatabase
55
import android.database.sqlite.SQLiteOpenHelper
66
import android.os.Build
77
import android.util.Base64
8+
import android.util.Log
9+
import com.google.common.cache.Cache
810
import com.mux.player.internal.Constants
11+
import com.mux.player.internal.Logger
12+
import com.mux.player.internal.createNoLogger
913
import com.mux.player.oneOf
1014
import java.io.Closeable
1115
import java.io.File
@@ -24,6 +28,7 @@ import java.net.URL
2428
internal class CacheDatastore(
2529
val context: Context,
2630
val maxDiskSize: Long = 256 * 1024 * 1024,
31+
val logger: Logger,
2732
) : Closeable {
2833

2934
companion object {
@@ -33,9 +38,13 @@ internal class CacheDatastore(
3338
}
3439

3540
private val dbGuard = Any()
41+
3642
// note - guarded by dbHelperGuard
3743
private var dbHelper: DbHelper? = null
3844

45+
// note - guarded by dbHelperGuard
46+
private var sqlDb: SQLiteDatabase? = null
47+
3948
/**
4049
* Opens the datastore, blocking until it is ready to use
4150
*
@@ -55,16 +64,15 @@ internal class CacheDatastore(
5564
ensureDirs()
5665

5766
val newHelper = DbHelper(context.applicationContext, indexDbDir())
58-
// acquire an extra reference until closed. prevents DB underneath from closing/reopening
59-
newHelper.writableDatabase.acquireReference()
6067
this.dbHelper = newHelper
68+
this.sqlDb = newHelper.writableDatabase
6169
}
6270
}
6371
}
6472

6573
fun isOpen(): Boolean {
6674
synchronized(dbGuard) {
67-
return dbHelper != null
75+
return dbHelper != null && (sqlDb?.isOpen ?: false)
6876
}
6977
}
7078

@@ -94,13 +102,11 @@ internal class CacheDatastore(
94102
}
95103

96104
fun writeFileRecord(fileRecord: FileRecord): Result<Unit> {
97-
val rowId = databaseOrThrow().use {
98-
it.insertWithOnConflict(
99-
IndexSql.Files.name, null,
100-
fileRecord.toContentValues(),
101-
SQLiteDatabase.CONFLICT_REPLACE
102-
)
103-
}
105+
val rowId = databaseOrThrow().insertWithOnConflict(
106+
IndexSql.Files.name, null,
107+
fileRecord.toContentValues(),
108+
SQLiteDatabase.CONFLICT_REPLACE
109+
)
104110

105111
return if (rowId >= 0) {
106112
Result.success(Unit)
@@ -110,32 +116,47 @@ internal class CacheDatastore(
110116
}
111117

112118
fun readRecordByLookupKey(key: String): FileRecord? {
113-
return databaseOrThrow().use {
114-
it.query(
115-
IndexSql.Files.name, null,
116-
"${IndexSql.Files.Columns.lookupKey} is ?",
117-
arrayOf(key),
118-
null, null, null
119-
).use { cursor ->
120-
if (cursor.count > 0 && cursor.moveToFirst()) {
121-
cursor.toFileRecord()
122-
} else {
123-
null
124-
}
119+
return databaseOrThrow().query(
120+
IndexSql.Files.name, null,
121+
"${IndexSql.Files.Columns.lookupKey} is ?",
122+
arrayOf(key),
123+
null, null, null
124+
).use { cursor ->
125+
if (cursor.count > 0 && cursor.moveToFirst()) {
126+
cursor.toFileRecord()
127+
} else {
128+
null
125129
}
126130
}
127131
}
128132

129133
fun readRecordByUrl(url: String): FileRecord? {
130-
return databaseOrThrow().use {
131-
it.query(
134+
logger.i(
135+
TAG,
136+
"readRecordByUrl() tid=${Thread.currentThread().id} called for datastore $this\n\tfor url $url"
137+
)
138+
return databaseOrThrow().run {
139+
query(
132140
IndexSql.Files.name, null,
133141
"${IndexSql.Files.Columns.lookupKey} is ?",
134142
arrayOf(safeCacheKey(URL(url))),
135143
null, null, null
136144
).use { cursor ->
145+
logger.d(
146+
TAG,
147+
"readRecordByUrl() tid=${Thread.currentThread().id} about to talk to the cursor $this\n\t btw is it closed according to Cursor? ${cursor.isClosed}"
148+
)
149+
logger.i(
150+
TAG,
151+
"readRecordByUrl() tid=${Thread.currentThread().id} \n\tcursor closed ${cursor.isClosed}\n\t db closed ${!isOpen}"
152+
)
137153
if (cursor.count > 0 && cursor.moveToFirst()) {
138-
cursor.toFileRecord()
154+
cursor.toFileRecord().also {
155+
logger.i(
156+
TAG,
157+
"readRecordByUrl() tid=${Thread.currentThread().id} returning with record\n\tfor $url"
158+
)
159+
}
139160
} else {
140161
null
141162
}
@@ -176,18 +197,18 @@ internal class CacheDatastore(
176197
* recently-used items
177198
*/
178199
fun evictByLru(): Result<Int> {
179-
return databaseOrThrow().use { doEvictByLru(it) }
200+
return doEvictByLru(databaseOrThrow())
180201

181202
}
182203

183204
@Throws(IOException::class)
184205
override fun close() {
206+
logger.i(TAG, "close() tid=${Thread.currentThread().id} called for datastore $this")
185207
synchronized(dbGuard) {
186-
// release reference from when we opened. if any loader threads are reading/writing then
187-
// the db will close once they wind down
188-
dbHelper?.writableDatabase?.releaseReference()
208+
sqlDb?.close()
189209
dbHelper?.close()
190210
dbHelper = null
211+
sqlDb = null
191212
}
192213
}
193214

@@ -198,7 +219,7 @@ internal class CacheDatastore(
198219
internal fun readLeastRecentFiles(): List<FileRecord> {
199220
// This function is only visible for testing. doReadLeastRecentFiles() is called internally in
200221
// a transaction with an already-open db
201-
return databaseOrThrow().use { doReadLeastRecentFiles(it) }
222+
return doReadLeastRecentFiles(databaseOrThrow())
202223
}
203224

204225
/**
@@ -383,8 +404,7 @@ internal class CacheDatastore(
383404
@Throws(IOException::class)
384405
private fun databaseOrThrow(): SQLiteDatabase {
385406
synchronized(dbGuard) {
386-
val helper = dbHelper ?: throw IOException("CacheDatastore was closed")
387-
return helper.writableDatabase
407+
return sqlDb?.takeIf { it.isOpen } ?: throw IOException("CacheDatastore was closed")
388408
}
389409
}
390410
}
@@ -400,6 +420,7 @@ private class DbHelper(
400420
) {
401421

402422
companion object {
423+
private const val TAG = "CacheDatastore"
403424
private const val DB_FILE = "mux-player-cache.db"
404425
}
405426

library/src/main/java/com/mux/player/internal/cache/MuxPlayerCache.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.mux.player.internal.cache
22

33
import android.content.Context
4+
import com.mux.player.internal.Logger
5+
import com.mux.player.internal.createNoLogger
46
import java.io.BufferedInputStream
57
import java.io.BufferedOutputStream
68
import java.io.Closeable
@@ -21,9 +23,11 @@ import java.util.concurrent.TimeUnit
2123
*/
2224
class MuxPlayerCache private constructor(
2325
private val datastore: CacheDatastore,
26+
private val logger: Logger,
2427
) {
2528

26-
constructor(appContext: Context) : this(CacheDatastore(appContext.applicationContext))
29+
constructor(appContext: Context, logger: Logger)
30+
: this(CacheDatastore(appContext.applicationContext, logger = logger), logger)
2731

2832
companion object {
2933
private const val TAG = "CacheController"
@@ -34,9 +38,10 @@ class MuxPlayerCache private constructor(
3438
val RX_S_MAX_AGE = Regex("""s-max-age=([0-9].*)""")
3539

3640
@JvmSynthetic
37-
internal fun create(appContext: Context) = MuxPlayerCache(appContext)
41+
internal fun create(appContext: Context, logger: Logger) = MuxPlayerCache(appContext, logger)
3842
@JvmSynthetic
39-
internal fun create(datastore: CacheDatastore) = MuxPlayerCache(datastore)
43+
internal fun create(datastore: CacheDatastore, logger: Logger) =
44+
MuxPlayerCache(datastore, logger)
4045
}
4146

4247
/**

library/src/test/java/com/mux/player/CacheDatastoreTests.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.mux.player
22

33
import android.content.Context
44
import com.mux.player.internal.cache.CacheDatastore
5+
import com.mux.player.internal.createNoLogger
56
import io.mockk.every
67
import io.mockk.mockk
78
import org.junit.Assert
@@ -21,7 +22,7 @@ class CacheDatastoreTests : AbsRobolectricTest() {
2122
}
2223
}
2324

24-
cacheDatastore = CacheDatastore(mockContext)
25+
cacheDatastore = CacheDatastore(mockContext, logger = createNoLogger())
2526
}
2627

2728
@Test

library/src/test/java/com/mux/player/MuxPlayerCacheTests.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.mux.player
33
import android.content.Context
44
import com.mux.player.internal.cache.MuxPlayerCache
55
import com.mux.player.internal.cache.CacheDatastore
6+
import com.mux.player.internal.createNoLogger
67
import io.mockk.every
78
import io.mockk.mockk
89
import org.junit.Assert
@@ -24,7 +25,7 @@ class MuxPlayerCacheTests : AbsRobolectricTest() {
2425
}
2526
}
2627

27-
muxPlayerCache = MuxPlayerCache.create(mockDatastore)
28+
muxPlayerCache = MuxPlayerCache.create(mockDatastore, logger = createNoLogger())
2829
}
2930

3031
@Test

0 commit comments

Comments
 (0)