-
-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor media controller into view model
- Loading branch information
Showing
7 changed files
with
252 additions
and
119 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
app/src/main/kotlin/org/akanework/gramophone/logic/utils/LifecycleCallbackList.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package org.akanework.gramophone.logic.utils | ||
|
||
import androidx.lifecycle.DefaultLifecycleObserver | ||
import androidx.lifecycle.Lifecycle | ||
import androidx.lifecycle.LifecycleOwner | ||
|
||
interface LifecycleCallbackList<T> { | ||
fun addCallbackForever(clear: Boolean = false, callback: T) { | ||
addCallback(null, clear, callback) | ||
} | ||
fun addCallback(lifecycle: Lifecycle?, clear: Boolean = false, callback: T) | ||
fun removeCallback(callback: T) | ||
} | ||
|
||
class LifecycleCallbackListImpl<T>(lifecycle: Lifecycle? = null) | ||
: LifecycleCallbackList<T>, DefaultLifecycleObserver { | ||
private var list = hashMapOf<T, Pair<Boolean, CallbackLifecycleObserver?>>() | ||
|
||
init { | ||
lifecycle?.addObserver(this) | ||
} | ||
|
||
override fun addCallback(lifecycle: Lifecycle?, clear: Boolean, callback: T) { | ||
list[callback] = Pair(clear, lifecycle?.let { CallbackLifecycleObserver(it, callback) }) | ||
} | ||
|
||
override fun removeCallback(callback: T) { | ||
list.remove(callback)?.second?.release() | ||
} | ||
|
||
fun dispatch(callback: (T) -> Unit) { | ||
list.forEach { callback(it.key) } | ||
list = HashMap(list.filterValues { !it.first }) | ||
} | ||
|
||
fun release() { | ||
dispatch { removeCallback(it) } | ||
} | ||
|
||
fun throwIfRelease() { | ||
if (list.size == 0) return | ||
release() | ||
throw IllegalStateException("Callbacks leaked in LifecycleCallbackList") | ||
} | ||
|
||
fun iterator(): Iterator<T> { | ||
return list.keys.iterator() | ||
} | ||
|
||
override fun onDestroy(owner: LifecycleOwner) { | ||
release() | ||
} | ||
|
||
private inner class CallbackLifecycleObserver(private val lifecycle: Lifecycle, | ||
private val callback: T) | ||
: DefaultLifecycleObserver { | ||
|
||
init { | ||
lifecycle.addObserver(this) | ||
} | ||
|
||
override fun onDestroy(owner: LifecycleOwner) { | ||
removeCallback(callback) | ||
} | ||
|
||
fun release() { | ||
lifecycle.removeObserver(this) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
116 changes: 116 additions & 0 deletions
116
app/src/main/kotlin/org/akanework/gramophone/ui/MediaControllerViewModel.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package org.akanework.gramophone.ui | ||
|
||
import android.app.Application | ||
import android.content.ComponentName | ||
import android.os.Bundle | ||
import androidx.core.content.ContextCompat | ||
import androidx.lifecycle.AndroidViewModel | ||
import androidx.lifecycle.DefaultLifecycleObserver | ||
import androidx.lifecycle.Lifecycle | ||
import androidx.lifecycle.LifecycleOwner | ||
import androidx.media3.session.MediaController | ||
import androidx.media3.session.SessionCommand | ||
import androidx.media3.session.SessionResult | ||
import androidx.media3.session.SessionToken | ||
import com.google.common.util.concurrent.ListenableFuture | ||
import org.akanework.gramophone.logic.GramophoneApplication | ||
import org.akanework.gramophone.logic.GramophonePlaybackService | ||
import org.akanework.gramophone.logic.utils.LifecycleCallbackList | ||
import org.akanework.gramophone.logic.utils.LifecycleCallbackListImpl | ||
|
||
class MediaControllerViewModel(application: Application) : AndroidViewModel(application), | ||
DefaultLifecycleObserver, MediaController.Listener { | ||
|
||
private val context: GramophoneApplication | ||
get() = getApplication() | ||
private var controllerFuture: ListenableFuture<MediaController>? = null | ||
private val customCommandListenersImpl = LifecycleCallbackListImpl< | ||
(MediaController, SessionCommand, Bundle) -> ListenableFuture<SessionResult>>() | ||
private val disconnectionListenersImpl = LifecycleCallbackListImpl<() -> Unit>() | ||
private val connectionListenersImpl = LifecycleCallbackListImpl<(MediaController) -> Unit>() | ||
val customCommandListeners: LifecycleCallbackList< | ||
(MediaController, SessionCommand, Bundle) -> ListenableFuture<SessionResult>> | ||
get() = customCommandListenersImpl | ||
val disconnectionListeners: LifecycleCallbackList<() -> Unit> | ||
get() = disconnectionListenersImpl | ||
val connectionListeners: LifecycleCallbackList<(MediaController) -> Unit> | ||
get() = connectionListenersImpl | ||
|
||
override fun onStart(owner: LifecycleOwner) { | ||
val sessionToken = | ||
SessionToken(context, ComponentName(context, GramophonePlaybackService::class.java)) | ||
controllerFuture = | ||
MediaController | ||
.Builder(context, sessionToken) | ||
.setListener(this) | ||
.buildAsync() | ||
.apply { | ||
addListener( | ||
{ | ||
if (controllerFuture?.isDone == true && | ||
controllerFuture?.isCancelled == false) { | ||
val instance = get() | ||
connectionListenersImpl.dispatch { it(instance) } | ||
} | ||
}, ContextCompat.getMainExecutor(context) | ||
) | ||
} | ||
} | ||
|
||
fun addOneOffControllerCallback(lifecycle: Lifecycle?, callback: (MediaController) -> Unit) { | ||
val instance = get() | ||
if (instance != null) { | ||
callback(instance) | ||
} else { | ||
connectionListeners.addCallback(lifecycle, true, callback) | ||
} | ||
} | ||
|
||
fun get(): MediaController? { | ||
if (controllerFuture?.isDone == true && controllerFuture?.isCancelled == false) { | ||
return controllerFuture!!.get() | ||
} | ||
return null | ||
} | ||
|
||
override fun onDisconnected(controller: MediaController) { | ||
controllerFuture = null | ||
disconnectionListenersImpl.dispatch { it() } | ||
} | ||
|
||
override fun onStop(owner: LifecycleOwner) { | ||
if (controllerFuture?.isDone == true) { | ||
if (controllerFuture?.isCancelled == false) { | ||
controllerFuture?.get()?.release() | ||
} else { | ||
throw IllegalStateException("controllerFuture?.isCancelled != false") | ||
} | ||
} else { | ||
controllerFuture?.cancel(true) | ||
controllerFuture = null | ||
disconnectionListenersImpl.dispatch { it() } | ||
} | ||
} | ||
|
||
override fun onDestroy(owner: LifecycleOwner) { | ||
ContextCompat.getMainExecutor(context).execute { | ||
customCommandListenersImpl.throwIfRelease() | ||
connectionListenersImpl.throwIfRelease() | ||
disconnectionListenersImpl.throwIfRelease() | ||
} | ||
} | ||
|
||
override fun onCustomCommand( | ||
controller: MediaController, | ||
command: SessionCommand, | ||
args: Bundle | ||
): ListenableFuture<SessionResult> { | ||
var future: ListenableFuture<SessionResult>? = null | ||
val listenerIterator = customCommandListenersImpl.iterator() | ||
while (future == null || (future.isDone && | ||
future.get().resultCode == SessionResult.RESULT_ERROR_NOT_SUPPORTED)) { | ||
future = listenerIterator.next()(controller, command, args) | ||
} | ||
return future | ||
} | ||
} |
Oops, something went wrong.