Skip to content

Commit 2f32929

Browse files
committed
PlayerUiList: guard list actions with mutex
The new implementation would throw `ConcurrentModificationExceptions` when destroying the UIs. So let’s play it safe and put the list behind a mutex. Adds a helper class `GuardedByMutex` that can be wrapped around a property to force all use-sites to acquire the lock before doing anything with the data.
1 parent 5b92de4 commit 2f32929

File tree

2 files changed

+87
-19
lines changed

2 files changed

+87
-19
lines changed

app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.kt

+40-19
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package org.schabi.newpipe.player.ui
22

3-
import androidx.core.util.Consumer
3+
import org.schabi.newpipe.util.GuardedByMutex
44
import java.util.Optional
55

66
class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
7-
val playerUis = mutableListOf<PlayerUi>()
7+
var playerUis = GuardedByMutex(mutableListOf<PlayerUi>())
88

99
/**
1010
* Creates a [PlayerUiList] starting with the provided player uis. The provided player uis
@@ -16,7 +16,9 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
1616
* @param initialPlayerUis the player uis this list should start with; the order will be kept
1717
*/
1818
init {
19-
playerUis.addAll(listOf(*initialPlayerUis))
19+
playerUis.runWithLockSync {
20+
lockData.addAll(listOf(*initialPlayerUis))
21+
}
2022
}
2123

2224
/**
@@ -41,7 +43,9 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
4143
}
4244
}
4345

44-
playerUis.add(playerUi)
46+
playerUis.runWithLockSync {
47+
lockData.add(playerUi)
48+
}
4549
}
4650

4751
/**
@@ -52,12 +56,24 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
5256
* @param T the class type parameter </T>
5357
* */
5458
fun <T> destroyAll(playerUiType: Class<T?>) {
55-
for (ui in playerUis) {
56-
if (playerUiType.isInstance(ui)) {
57-
ui.destroyPlayer()
58-
ui.destroy()
59-
playerUis.remove(ui)
59+
val toDestroy = mutableListOf<PlayerUi>()
60+
61+
// short blocking removal from class to prevent interfering from other threads
62+
playerUis.runWithLockSync {
63+
val new = mutableListOf<PlayerUi>()
64+
for (ui in lockData) {
65+
if (playerUiType.isInstance(ui)) {
66+
toDestroy.add(ui)
67+
} else {
68+
new.add(ui)
69+
}
6070
}
71+
lockData = new
72+
}
73+
// then actually destroy the UIs
74+
for (ui in toDestroy) {
75+
ui.destroyPlayer()
76+
ui.destroy()
6177
}
6278
}
6379

@@ -67,18 +83,19 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
6783
* @param T the class type parameter
6884
* @return the first player UI of the required type found in the list, or null
6985
</T> */
70-
fun <T> get(playerUiType: Class<T>): T? {
71-
for (ui in playerUis) {
72-
if (playerUiType.isInstance(ui)) {
73-
when (val r = playerUiType.cast(ui)) {
74-
// try all UIs before returning null
75-
null -> continue
76-
else -> return r
86+
fun <T> get(playerUiType: Class<T>): T? =
87+
playerUis.runWithLockSync {
88+
for (ui in lockData) {
89+
if (playerUiType.isInstance(ui)) {
90+
when (val r = playerUiType.cast(ui)) {
91+
// try all UIs before returning null
92+
null -> continue
93+
else -> return@runWithLockSync r
94+
}
7795
}
7896
}
97+
return@runWithLockSync null
7998
}
80-
return null
81-
}
8299

83100
/**
84101
* @param playerUiType the class of the player UI to return;
@@ -96,7 +113,11 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
96113
* @param consumer the consumer to call with player UIs
97114
*/
98115
fun call(consumer: java.util.function.Consumer<PlayerUi>) {
99-
for (ui in playerUis) {
116+
// copy the list out of the mutex before calling the consumer which might block
117+
val new = playerUis.runWithLockSync {
118+
lockData.toMutableList()
119+
}
120+
for (ui in new) {
100121
consumer.accept(ui)
101122
}
102123
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.schabi.newpipe.util
2+
3+
import kotlinx.coroutines.runBlocking
4+
import kotlinx.coroutines.sync.Mutex
5+
import kotlinx.coroutines.sync.withLock
6+
7+
/** Guard the given data so that it can only be accessed by locking the mutex first.
8+
*
9+
* Inspired by [this blog post](https://jonnyzzz.com/blog/2017/03/01/guarded-by-lock/)
10+
* */
11+
class GuardedByMutex<T>(
12+
private var data: T,
13+
private val lock: Mutex = Mutex(locked = false),
14+
) {
15+
16+
/** Lock the mutex and access the data, blocking the current thread.
17+
* @param action to run with locked mutex
18+
* */
19+
fun <Y> runWithLockSync(
20+
action: MutexData<T>.() -> Y
21+
) =
22+
runBlocking {
23+
lock.withLock {
24+
MutexData(data, { d -> data = d }).action()
25+
}
26+
}
27+
28+
/** Lock the mutex and access the data, suspending the coroutine.
29+
* @param action to run with locked mutex
30+
* */
31+
suspend fun <Y> runWithLock(action: MutexData<T>.() -> Y) =
32+
lock.withLock {
33+
MutexData(data, { d -> data = d }).action()
34+
}
35+
}
36+
37+
/** The data inside a [GuardedByMutex], which can be accessed via [lockData].
38+
* [lockData] is a `var`, so you can `set` it as well.
39+
* */
40+
class MutexData<T>(data: T, val setFun: (T) -> Unit) {
41+
/** The data inside this [GuardedByMutex] */
42+
var lockData: T = data
43+
set(t: T) {
44+
setFun(t)
45+
field = t
46+
}
47+
}

0 commit comments

Comments
 (0)