diff --git a/buildSrc/src/main/java/kohii/Dependencies.kt b/buildSrc/src/main/java/kohii/Dependencies.kt index d496ca6a..27aef77b 100644 --- a/buildSrc/src/main/java/kohii/Dependencies.kt +++ b/buildSrc/src/main/java/kohii/Dependencies.kt @@ -155,6 +155,7 @@ object Libs { val liveData = "androidx.lifecycle:lifecycle-livedata-ktx:$version" val viewModel = "androidx.lifecycle:lifecycle-viewmodel-ktx:$version" val service = "androidx.lifecycle:lifecycle-service:$version" + val process = "androidx.lifecycle:lifecycle-process:$version" } object Room { diff --git a/kohii-core/build.gradle b/kohii-core/build.gradle index c57b41f2..e03ff45e 100644 --- a/kohii-core/build.gradle +++ b/kohii-core/build.gradle @@ -74,6 +74,7 @@ dependencies { implementation Libs.AndroidX.coordinatorLayout implementation Libs.AndroidX.Lifecycle.extensions implementation Libs.AndroidX.Lifecycle.service + implementation Libs.AndroidX.Lifecycle.process api Libs.AndroidX.Lifecycle.java8 testImplementation(Libs.Common.junit) diff --git a/kohii-core/src/main/java/kohii/v1/core/AbstractPlayable.kt b/kohii-core/src/main/java/kohii/v1/core/AbstractPlayable.kt index 8bb4bdd4..dad4ba3b 100644 --- a/kohii-core/src/main/java/kohii/v1/core/AbstractPlayable.kt +++ b/kohii-core/src/main/java/kohii/v1/core/AbstractPlayable.kt @@ -22,6 +22,7 @@ import kohii.v1.core.MemoryMode.HIGH import kohii.v1.core.MemoryMode.INFINITE import kohii.v1.core.MemoryMode.LOW import kohii.v1.core.MemoryMode.NORMAL +import kohii.v1.debugOnly import kohii.v1.internal.PlayerParametersChangeListener import kohii.v1.logInfo import kohii.v1.logWarn @@ -41,7 +42,7 @@ abstract class AbstractPlayable( private var playRequested: Boolean = false override fun toString(): String { - return "Playable([t=$tag][b=$bridge][h=${super.hashCode()}])" + return "Playable([t=$tag][b=$bridge][h=${hashCode()}])" } // Ensure the preparation for the playback @@ -89,6 +90,7 @@ abstract class AbstractPlayable( } override fun isPlaying(): Boolean { + "Playable#isPlaying $this".logInfo() return bridge.isPlaying() } @@ -102,12 +104,12 @@ abstract class AbstractPlayable( val newManager = field if (oldManager === newManager) return "Playable#manager $oldManager --> $newManager, $this".logInfo() + oldManager?.removePlayable(this) + newManager?.addPlayable(this) + // Setting Manager to null. if (newManager == null) { master.trySavePlaybackInfo(this) - master.tearDown( - playable = this, - clearState = if (oldManager is Manager) !oldManager.isChangingConfigurations() else true - ) + master.tearDown(playable = this) } else if (oldManager === null) { master.tryRestorePlaybackInfo(this) } @@ -120,15 +122,7 @@ abstract class AbstractPlayable( val newPlayback = field if (oldPlayback === newPlayback) return "Playable#playback $oldPlayback --> $newPlayback, $this".logInfo() - if (oldPlayback != null) { - bridge.removeErrorListener(oldPlayback) - bridge.removeEventListener(oldPlayback) - oldPlayback.removeCallback(this) - if (oldPlayback.playable === this) oldPlayback.playable = null - if (oldPlayback.playerParametersChangeListener === this) { - oldPlayback.playerParametersChangeListener = null - } - } + oldPlayback?.let(::detachFromPlayback) this.manager = if (newPlayback != null) { newPlayback.manager @@ -150,23 +144,7 @@ abstract class AbstractPlayable( } } - if (newPlayback != null) { - newPlayback.playable = this - newPlayback.playerParametersChangeListener = this - newPlayback.addCallback(this) - newPlayback.config.callbacks.forEach { callback -> newPlayback.addCallback(callback) } - - bridge.addEventListener(newPlayback) - bridge.addErrorListener(newPlayback) - - if (newPlayback.tag != Master.NO_TAG) { - if (newPlayback.config.controller != null) { - master.plannedManualPlayables.add(newPlayback.tag) - } else { - master.plannedManualPlayables.remove(newPlayback.tag) - } - } - } + newPlayback?.let(::attachToPlayback) master.notifyPlaybackChanged(this, oldPlayback, newPlayback) } @@ -174,6 +152,42 @@ abstract class AbstractPlayable( override val playerState: Int get() = bridge.playerState + private fun attachToPlayback(playback: Playback) { + playback.playable = this + playback.playerParametersChangeListener = this + playback.addCallback(this) + playback.config.callbacks.forEach { callback -> playback.addCallback(callback) } + + bridge.addEventListener(playback) + bridge.addErrorListener(playback) + + if (playback.tag != Master.NO_TAG) { + if (playback.config.controller != null) { + master.plannedManualPlayables.add(playback.tag) + } else { + master.plannedManualPlayables.remove(playback.tag) + } + } + } + + private fun detachFromPlayback(playback: Playback) { + bridge.removeErrorListener(playback) + bridge.removeEventListener(playback) + playback.removeCallback(this) + debugOnly { + check(playback.playable === this) { + """ + Old playback of this playable ($this) is + bound to a different playable: ${playback.playable} + """.trimIndent() + } + } + if (playback.playable === this) playback.playable = null + if (playback.playerParametersChangeListener === this) { + playback.playerParametersChangeListener = null + } + } + // Playback.Callback override fun onActive(playback: Playback) { diff --git a/kohii-core/src/main/java/kohii/v1/core/Binder.kt b/kohii-core/src/main/java/kohii/v1/core/Binder.kt index 53adbd8d..3b2fd694 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Binder.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Binder.kt @@ -86,7 +86,7 @@ class Binder( if (cache.config != config /* equals */) { // Scenario: client bind a Video of same tag/media but different Renderer type or Config. cache.playback = null // will also set Manager to null - engine.master.tearDown(cache, true) + engine.master.tearDown(cache) cache = null } } diff --git a/kohii-core/src/main/java/kohii/v1/core/Bridge.kt b/kohii-core/src/main/java/kohii/v1/core/Bridge.kt index 8a36ee6d..af0e9740 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Bridge.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Bridge.kt @@ -56,7 +56,11 @@ interface Bridge { */ fun prepare(loadSource: Boolean) - // Ensure resource is ready to play. PlaybackDispatcher will require this for manual playback. + /** + * Ensure the resources is ready to play. + * + * Note: PlaybackDispatcher will require this for manual playback. + */ fun ready() fun play() diff --git a/kohii-core/src/main/java/kohii/v1/core/CompatHelper.kt b/kohii-core/src/main/java/kohii/v1/core/CompatHelper.kt new file mode 100644 index 00000000..fb69583d --- /dev/null +++ b/kohii-core/src/main/java/kohii/v1/core/CompatHelper.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.core + +import androidx.annotation.MainThread + +object CompatHelper { + + private val defaultConfig: CompatConfig = CompatConfig() + private var customConfig: CompatConfig? = null + + val compatConfig: CompatConfig get() = customConfig ?: defaultConfig + + @MainThread + fun setCompatConfig(compatConfig: CompatConfig) { + check(customConfig == null || customConfig == compatConfig) { + "Another CompatConfig is already set." + } + customConfig = compatConfig + } +} + +data class CompatConfig( + val useLegacyPlaybackInfoStore: Boolean = false +) diff --git a/kohii-core/src/main/java/kohii/v1/core/Engine.kt b/kohii-core/src/main/java/kohii/v1/core/Engine.kt index 21cda699..df7fcd81 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Engine.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Engine.kt @@ -27,12 +27,14 @@ import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle.State import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelProvider import kohii.v1.core.Binder.Options import kohii.v1.core.MemoryMode.LOW import kohii.v1.core.Scope.BUCKET import kohii.v1.core.Scope.GROUP import kohii.v1.core.Scope.MANAGER import kohii.v1.core.Scope.PLAYBACK +import kohii.v1.internal.ManagerViewModel import kohii.v1.media.Media import kohii.v1.media.MediaItem import kohii.v1.media.VolumeInfo @@ -47,9 +49,7 @@ abstract class Engine constructor( playableCreator: PlayableCreator ) : this(Master[context], playableCreator) - internal fun inject(group: Group) { - group.managers.forEach { prepare(it) } - } + internal fun inject(group: Group) = group.managers.forEach(::prepare) abstract fun prepare(manager: Manager) @@ -103,6 +103,7 @@ abstract class Engine constructor( activity = activity, host = fragment, managerLifecycleOwner = lifecycleOwner, + viewModel = ViewModelProvider(fragment).get(ManagerViewModel::class.java), memoryMode = memoryMode, activeLifecycleState = activeLifecycleState ) @@ -117,6 +118,7 @@ abstract class Engine constructor( activity = activity, host = activity, managerLifecycleOwner = activity, + viewModel = ViewModelProvider(activity).get(ManagerViewModel::class.java), memoryMode = memoryMode, activeLifecycleState = activeLifecycleState ) diff --git a/kohii-core/src/main/java/kohii/v1/core/Group.kt b/kohii-core/src/main/java/kohii/v1/core/Group.kt index 63f1775b..f8ebb2b6 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Group.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Group.kt @@ -18,6 +18,7 @@ package kohii.v1.core import android.graphics.Rect import android.os.Handler +import android.os.Looper import android.os.Message import android.view.ViewGroup import androidx.collection.arraySetOf @@ -51,20 +52,21 @@ class Group( private var stickyManager: Manager? = null set(value) { - val from = field + val prevStickyManager = field field = value - val to = field - if (from === to) return - if (to != null) { // a Manager is promoted - to.sticky = true - managers.push(to) + val nextStickyManager = field + if (prevStickyManager === nextStickyManager) return + if (nextStickyManager != null) { // a Manager is promoted + nextStickyManager.sticky = true + managers.push(nextStickyManager) } else { - require(from != null && from.sticky) - if (managers.peek() === from) { - from.sticky = false + require(prevStickyManager != null && prevStickyManager.sticky) + if (managers.peek() === prevStickyManager) { + prevStickyManager.sticky = false managers.pop() } } + onRefresh() } internal var groupVolumeInfo: VolumeInfo = VolumeInfo.DEFAULT_ACTIVE @@ -83,7 +85,7 @@ class Group( managers.forEach { it.lock = value } } - private val handler = Handler(this) + private val handler = Handler(/* looper */ Looper.getMainLooper(), /* callback */ this) private val dispatcher = PlayableDispatcher(master) private val playbacks: Collection @@ -157,8 +159,8 @@ class Group( toPlay.addAll(canPlay) toPause.addAll(canPause) } else { - managers.forEach { - val (canPlay, canPause) = it.splitPlaybacks() + for (manager in managers) { + val (canPlay, canPause) = manager.splitPlaybacks() toPlay.addAll(canPlay) toPause.addAll(canPause) } @@ -190,6 +192,7 @@ class Group( if (it.host is OnSelectionListener) { it.host to (grouped[it] ?: emptyList()) } else { + @Suppress("USELESS_CAST") null as Pair>? } } @@ -231,7 +234,6 @@ class Group( internal fun onManagerDestroyed(manager: Manager) { if (stickyManager === manager) stickyManager = null if (managers.remove(manager)) master.onGroupUpdated(this) - if (managers.size == 0) master.onLastManagerDestroyed(this) } // This operation should: diff --git a/kohii-core/src/main/java/kohii/v1/core/Interfaces.kt b/kohii-core/src/main/java/kohii/v1/core/Interfaces.kt index 592ba719..8b6b6d78 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Interfaces.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Interfaces.kt @@ -180,6 +180,4 @@ interface DefaultTrackSelectorHolder { val trackSelector: DefaultTrackSelector } -interface PlayableManager - interface PlayableContainer diff --git a/kohii-core/src/main/java/kohii/v1/core/Manager.kt b/kohii-core/src/main/java/kohii/v1/core/Manager.kt index 3ac4d0e5..f7853d66 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Manager.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Manager.kt @@ -34,6 +34,7 @@ import kohii.v1.core.Scope.GROUP import kohii.v1.core.Scope.MANAGER import kohii.v1.core.Scope.PLAYBACK import kohii.v1.core.Strategy.SINGLE_PLAYER +import kohii.v1.internal.ManagerViewModel import kohii.v1.logDebug import kohii.v1.logInfo import kohii.v1.media.VolumeInfo @@ -64,6 +65,7 @@ class Manager internal constructor( internal val group: Group, val host: Any, internal val lifecycleOwner: LifecycleOwner, + internal val viewModel: ManagerViewModel, internal val memoryMode: MemoryMode = LOW, internal val activeLifecycleState: State = State.STARTED ) : PlayableManager, DefaultLifecycleObserver, LifecycleEventObserver, Comparable { @@ -104,16 +106,18 @@ class Manager internal constructor( // Up to one Bucket can be sticky at a time. private var stickyBucket: Bucket? = null set(value) { - val from = field + val prevStickyBucket = field field = value - val to = field - if (from === to) return - // Promote 'to' from buckets. - if (to != null /* set new sticky Bucket */) { - buckets.push(to) // Push it to head. - } else { // 'to' is null then 'from' must be nonnull. Consider to remove it from head. - // Demote 'from' - if (buckets.peek() === from) buckets.pop() + val nextStickyBucket = field + if (prevStickyBucket === nextStickyBucket) return + // Promote 'nextStickyBucket' from buckets. + if (nextStickyBucket != null) { + // Set new sticky Bucket by pushing it to head. + buckets.push(nextStickyBucket) + } else { + // 'nextStickyBucket' is null then 'prevStickyBucket' must be nonnull. Consider to remove it + // from head. + if (buckets.peek() === prevStickyBucket) buckets.pop() } } @@ -145,11 +149,16 @@ class Manager internal constructor( } } + /** + * @see [LifecycleEventObserver.onStateChanged] + */ override fun onStateChanged( source: LifecycleOwner, event: Event ) { - playbacks.forEach { it.value.lifecycleState = source.lifecycle.currentState } + for ((_, playback) in playbacks) { + playback.lifecycleState = source.lifecycle.currentState + } refresh() } @@ -157,7 +166,8 @@ class Manager internal constructor( playbacks.values .toMutableList() .also { group.selection -= it } - .onEach { removePlayback(it) /* also modify 'playbacks' content */ } + // Remove the playback. This will also modify 'playbacks' content. + .onEach(::removePlayback) .clear() stickyBucket = null // will pop current sticky Bucket from the Stack @@ -186,6 +196,14 @@ class Manager internal constructor( refresh() } + override fun trySavePlaybackInfo(playable: Playable) { + viewModel.trySavePlaybackInfo(playable) + } + + override fun tryRestorePlaybackInfo(playable: Playable) { + viewModel.tryRestorePlaybackInfo(playable) + } + internal fun findRendererProvider(playable: Playable): RendererProvider { val cache = rendererProviders[playable.config.rendererType] ?: rendererProviders.entries.firstOrNull { @@ -271,8 +289,8 @@ class Manager internal constructor( val toInActive = playbacks.filterValues { it.isActive && !it.token.shouldPrepare() } .values - toActive.forEach { onPlaybackActive(it) } - toInActive.forEach { onPlaybackInActive(it) } + toActive.forEach(::onPlaybackActive) + toInActive.forEach(::onPlaybackInActive) return playbacks.entries.filter { it.value.isAttached } .partitionToMutableSets( @@ -348,6 +366,10 @@ class Manager internal constructor( playableObservers[playable.tag]?.invoke(playable.tag, from, to) } + override fun addPlayable(playable: Playable) = viewModel.addPlayable(playable) + + override fun removePlayable(playable: Playable) = viewModel.removePlayable(playable) + private fun onPlaybackAttached(playback: Playback): Unit = playback.onAttached() private fun onPlaybackDetached(playback: Playback): Unit = playback.onDetached() diff --git a/kohii-core/src/main/java/kohii/v1/core/Master.kt b/kohii-core/src/main/java/kohii/v1/core/Master.kt index c0129e94..24403b2f 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Master.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Master.kt @@ -29,12 +29,12 @@ import android.content.IntentFilter import android.content.res.Configuration import android.net.ConnectivityManager import android.os.Build.VERSION -import android.view.View import android.view.ViewGroup +import androidx.annotation.RestrictTo +import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX import androidx.collection.arrayMapOf import androidx.collection.arraySetOf import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle.State @@ -48,21 +48,20 @@ import kohii.v1.core.Binder.Options import kohii.v1.core.MemoryMode.AUTO import kohii.v1.core.MemoryMode.BALANCED import kohii.v1.core.MemoryMode.LOW -import kohii.v1.core.Playback.Config import kohii.v1.core.Scope.BUCKET import kohii.v1.core.Scope.GLOBAL import kohii.v1.core.Scope.GROUP import kohii.v1.core.Scope.MANAGER import kohii.v1.core.Scope.PLAYBACK import kohii.v1.findActivity -import kohii.v1.internal.DynamicFragmentRendererPlayback -import kohii.v1.internal.DynamicViewRendererPlayback +import kohii.v1.internal.BindRequest +import kohii.v1.internal.ManagerViewModel +import kohii.v1.internal.MasterDispatcher import kohii.v1.internal.MasterNetworkCallback -import kohii.v1.internal.StaticViewRendererPlayback import kohii.v1.logDebug import kohii.v1.logInfo -import kohii.v1.logWarn import kohii.v1.media.PlaybackInfo +import kohii.v1.utils.Capsule import java.util.concurrent.atomic.AtomicReference import kotlin.LazyThreadSafetyMode.NONE @@ -77,12 +76,10 @@ class Master private constructor(context: Context) : PlayableManager { internal val NO_TAG = Any() - @Volatile private var master: Master? = null + private val capsule = Capsule(::Master) @JvmStatic - operator fun get(context: Context) = master ?: synchronized(this) { - master ?: Master(context).also { master = it } - } + operator fun get(context: Context) = capsule.get(context) } val app = context.applicationContext as Application @@ -91,6 +88,7 @@ class Master private constructor(context: Context) : PlayableManager { internal val groups = mutableSetOf() internal val requests = mutableMapOf() internal val playables = mutableMapOf() + private val viewModels = mutableSetOf() // Memorize the tags which belongs to Playbacks that enable manual playbacks. On config change, // we may not get the binding of these tags yet, but we may need to play them. @@ -158,8 +156,7 @@ class Master private constructor(context: Context) : PlayableManager { field = value val to = field if (from == to) return - playables.filterKeys { it.playback?.isActive == true } - .forEach { it.key.onNetworkTypeChanged(from, to) } + playables.forEach { it.key.onNetworkTypeChanged(from, to) } } internal var trimMemoryLevel: Int = RunningAppProcessInfo().let { @@ -200,29 +197,40 @@ class Master private constructor(context: Context) : PlayableManager { activity: FragmentActivity, host: Any, managerLifecycleOwner: LifecycleOwner, + viewModel: ManagerViewModel, memoryMode: MemoryMode = AUTO, activeLifecycleState: State ): Manager { check(!activity.isDestroyed) { "Cannot register a destroyed Activity: $activity" } - val group = groups.find { it.activity === activity } ?: Group(this, activity).also { - onGroupCreated(it) - activity.lifecycle.addObserver(it) - } + val group = groups.find { it.activity === activity } ?: Group(this, activity) + .also { group -> + onGroupCreated(group) + activity.lifecycle.addObserver(group) + } return group.managers.find { it.lifecycleOwner === managerLifecycleOwner } - ?: Manager(this, group, host, managerLifecycleOwner, memoryMode, activeLifecycleState) - .also { - group.onManagerCreated(it) - managerLifecycleOwner.lifecycle.addObserver(it) - } + ?: Manager( + this, + group, + host, + managerLifecycleOwner, + viewModel, + memoryMode, + activeLifecycleState + ).also { manager -> + group.onManagerCreated(manager) + managerLifecycleOwner.lifecycle.addObserver(manager) + } } /** * @param container container is the [ViewGroup] that holds the Video. It should be an empty - * ViewGroup, or a PlayerView itself. Note that View can be created from [android.app.Service] so - * its Context doesn't need to be an [android.app.Activity] + * ViewGroup, or a player surface itself (e.g. the PlayerView instance). + * + * Note: View instance can be created from [android.app.Service] so its Context doesn't need to be + * an [android.app.Activity]. */ internal fun bind( playable: Playable, @@ -231,8 +239,9 @@ class Master private constructor(context: Context) : PlayableManager { options: Options, callback: ((Playback) -> Unit)? = null ) { - "Master#bind tag=$tag, playable=$playable, container=$container, options=$options".logInfo() + "Master#bind t=$tag, p=$playable, c=$container, o=$options".logInfo() // Remove any queued binding requests for the same container. + // FIXME: what if `MSG_BIND_PLAYABLE` is already consumed, but the container is not attached? dispatcher.removeMessages(MSG_BIND_PLAYABLE, container) // Remove any queued releasing request for the same Playable, since we are binding it now. dispatcher.removeMessages(MSG_RELEASE_PLAYABLE, playable) @@ -241,33 +250,33 @@ class Master private constructor(context: Context) : PlayableManager { // Keep track of which Playable will be bound to which Container. // Scenario: in RecyclerView, binding a Video in 'onBindViewHolder' will not immediately // trigger the binding, because we wait for the Container to be attached to the Window first. - // So if a Playable is registered to be bound, but then another Playable is registered to the + // So if a Playable is queued to bind, but then another Playable is queued to bind to the // same Container, we need to kick the previous Playable. - val requestForSameTag = requests.asSequence() + val keyForSameTag = requests.asSequence() .filter { it.value.tag !== NO_TAG } .firstOrNull { it.value.tag == tag } ?.key - if (requestForSameTag != null) requests.remove(requestForSameTag)?.onRemoved() + if (keyForSameTag != null) requests.remove(keyForSameTag)?.onRemoved() + + val keyForAnotherPlayable = requests.asSequence() + .filter { it.value.container === container && it.value.playable !== playable } + .firstOrNull() + ?.key + if (keyForAnotherPlayable != null) requests.remove(keyForAnotherPlayable)?.onRemoved() + requests[container] = BindRequest(this, playable, container, tag, options, callback) // if (playable.manager == null) playable.manager = this dispatcher.obtainMessage(MSG_BIND_PLAYABLE, container) .sendToTarget() } - internal fun tearDown( - playable: Playable, - clearState: Boolean - ) { + internal fun tearDown(playable: Playable) { dispatcher.removeMessages(MSG_DESTROY_PLAYABLE, playable) - dispatcher.obtainMessage(MSG_DESTROY_PLAYABLE, clearState.compareTo(true), -1, playable) - .sendToTarget() + dispatcher.obtainMessage(MSG_DESTROY_PLAYABLE, playable).sendToTarget() } - internal fun onTearDown( - playable: Playable, - clearState: Boolean - ) { - "Master#onTearDown: $playable, clear: $clearState".logDebug() + internal fun onTearDown(playable: Playable) { + "Master#onTearDown $playable".logDebug() check(playable.manager == null || playable.manager === this) { "Teardown $playable, found manager: ${playable.manager}" } @@ -284,7 +293,17 @@ class Master private constructor(context: Context) : PlayableManager { if (playables.isEmpty()) cleanUp() } - internal fun trySavePlaybackInfo(playable: Playable) { + // PlayableManager + + override fun addPlayable(playable: Playable) { + playables[playable] = playable.tag + } + + override fun removePlayable(playable: Playable) { + playables.remove(playable) + } + + override fun trySavePlaybackInfo(playable: Playable) { "Master#trySavePlaybackInfo: $playable".logDebug() val key = if (playable.tag !== NO_TAG) { playable.tag @@ -304,7 +323,7 @@ class Master private constructor(context: Context) : PlayableManager { } // If this method is called, it must be before any call to playable.bridge.prepare(flag) - internal fun tryRestorePlaybackInfo(playable: Playable) { + override fun tryRestorePlaybackInfo(playable: Playable) { "Master#tryRestorePlaybackInfo: $playable".logDebug() val cache = if (playable.tag !== NO_TAG) { playbackInfoStore.remove(playable.tag) @@ -343,8 +362,10 @@ class Master private constructor(context: Context) : PlayableManager { internal fun cleanupPendingPlayables() { playables.filter { it.key.manager === this } - .keys.toMutableList() + .keys + .toMutableList() .apply { + // FIXME(eneim): Re-think the manual playback mechanism. val manuallyStartedPlayable = manuallyStartedPlayable.get() if (manuallyStartedPlayable != null && manuallyStartedPlayable.isPlaying()) { minusAssign(manuallyStartedPlayable) @@ -355,7 +376,7 @@ class Master private constructor(context: Context) : PlayableManager { "$playable has manager: $this but found Playback: ${playable.playback}" } playable.manager = null - tearDown(playable, true) + tearDown(playable) } .clear() } @@ -390,11 +411,13 @@ class Master private constructor(context: Context) : PlayableManager { // Called when Manager is added (created)/removed (destroyed) to/from Group internal fun onGroupUpdated(group: Group) { - requests.values.filter { - val bucket = it.bucket - return@filter bucket != null && bucket.manager.group === group && - bucket.manager.lifecycleOwner.lifecycle.currentState < CREATED - } + requests.values + .filter { + val bucket = it.bucket + return@filter bucket != null && + bucket.manager.group === group && + bucket.manager.lifecycleOwner.lifecycle.currentState < CREATED + } .forEach { it.playable.playback = null requests.remove(it.container)?.onRemoved() @@ -406,34 +429,8 @@ class Master private constructor(context: Context) : PlayableManager { } } - internal fun onFirstManagerCreated(group: Group) { - if (groups.flatMap(Group::managers).isEmpty()) { - app.registerComponentCallbacks(componentCallbacks) - if (VERSION.SDK_INT >= 24 /* VERSION_CODES.N */) { - val networkManager = ContextCompat.getSystemService(app, ConnectivityManager::class.java) - networkManager?.registerDefaultNetworkCallback(networkCallback.value) - } else { - @Suppress("DEPRECATION") - app.registerReceiver( - networkActionReceiver.value, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) - ) - } - } + internal fun onFirstManagerCreated(group: Group): Unit = engines.forEach { it.value.inject(group) } - } - - @Suppress("UNUSED_PARAMETER") - internal fun onLastManagerDestroyed(group: Group) { - if (groups.flatMap(Group::managers).isEmpty()) { - if (networkCallback.isInitialized() && VERSION.SDK_INT >= 24 /* VERSION_CODES.N */) { - val networkManager = ContextCompat.getSystemService(app, ConnectivityManager::class.java) - networkManager?.unregisterNetworkCallback(networkCallback.value) - } else if (networkActionReceiver.isInitialized()) { - app.unregisterReceiver(networkActionReceiver.value) - } - app.unregisterComponentCallbacks(componentCallbacks) - } - } private fun cleanUp() { engines.forEach { it.value.cleanUp() } @@ -475,9 +472,43 @@ class Master private constructor(context: Context) : PlayableManager { } } - // Must be a request to play from Client. This method will set necessary flags and refresh all. - // Note: the last manual start wins. Which means that it will pause any other playing items and - // try to start the new one. + // Called before the viewModel is mapped to the Manager. + internal fun onViewModelCreated(viewModel: ManagerViewModel) { + val isFirstViewModel = viewModels.isEmpty() + viewModels.add(viewModel) + if (isFirstViewModel) { + app.registerComponentCallbacks(componentCallbacks) + if (VERSION.SDK_INT >= 24 /* VERSION_CODES.N */) { + val networkManager = ContextCompat.getSystemService(app, ConnectivityManager::class.java) + networkManager?.registerDefaultNetworkCallback(networkCallback.value) + } else { + @Suppress("DEPRECATION") + app.registerReceiver( + networkActionReceiver.value, + IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) + ) + } + } + } + + internal fun onViewModelCleared(viewModel: ManagerViewModel) { + viewModels.remove(viewModel) + if (viewModels.isEmpty()) { // The last Manager is cleared + if (networkCallback.isInitialized() && VERSION.SDK_INT >= 24 /* VERSION_CODES.N */) { + val networkManager = ContextCompat.getSystemService(app, ConnectivityManager::class.java) + networkManager?.unregisterNetworkCallback(networkCallback.value) + } else if (networkActionReceiver.isInitialized()) { + app.unregisterReceiver(networkActionReceiver.value) + } + app.unregisterComponentCallbacks(componentCallbacks) + } + } + + /** + * Called by [Manager.play] to start a [Playable] manually. This must be a request to play from + * Client. This method will set necessary flags and refresh all. Note: the last manual start wins. + * Which means that it will pause any other playing items and try to start the new one. + */ internal fun play(playable: Playable) { val tag = playable.tag if (tag == NO_TAG) return @@ -494,15 +525,19 @@ class Master private constructor(context: Context) : PlayableManager { } } - // Must be a request to pause from Client. This method will set necessary flags and refresh all. + /** + * Called by [Manager.pause] to pause a [Playable] manually. This must be a request to pause from + * Client. This method will set necessary flags and refresh all. + */ internal fun pause(playable: Playable) { val tag = playable.tag if (tag == NO_TAG) return - val controller = playable.playback?.config?.controller + val playback: Playback = playable.playback ?: return + val controller = playback.config.controller if (controller != null) { playablesPendingActions[tag] = Common.PAUSE manuallyStartedPlayable.set(null) - requireNotNull(playable.playback).manager.refresh() + playback.manager.refresh() } } @@ -626,6 +661,7 @@ class Master private constructor(context: Context) : PlayableManager { } } + @RestrictTo(LIBRARY_GROUP_PREFIX) fun registerEngine(engine: Engine<*>) { engines.put(engine.playableCreator.rendererType, engine) ?.cleanUp() @@ -702,75 +738,6 @@ class Master private constructor(context: Context) : PlayableManager { callback?.invoke(resolvedPlayback) } - internal class BindRequest( - val master: Master, - val playable: Playable, - val container: ViewGroup, - val tag: Any, - val options: Options, - val callback: ((Playback) -> Unit)? - ) { - - // used by RecyclerViewBucket to 'assume' that it will hold this container - // It is recommended to use Engine#cancel to easily remove a queued request from cache. - internal var bucket: Bucket? = null - - internal fun onBind() { - val bucket = master.findBucketForContainer(container) - - requireNotNull(bucket) { "No Manager and Bucket available for $container" } - - master.onBind(playable, tag, bucket.manager, container, callback, createNewPlayback@{ - val config = Config( - tag = options.tag, - delay = options.delay, - threshold = options.threshold, - preload = options.preload, - repeatMode = options.repeatMode, - controller = options.controller, - initialPlaybackInfo = options.initialPlaybackInfo, - artworkHintListener = options.artworkHintListener, - tokenUpdateListener = options.tokenUpdateListener, - networkTypeChangeListener = options.networkTypeChangeListener, - callbacks = options.callbacks - ) - - return@createNewPlayback when { - // Scenario: Playable accepts renderer of type PlayerView, and - // the container is an instance of PlayerView or its subtype. - playable.config.rendererType.isAssignableFrom(container.javaClass) -> { - StaticViewRendererPlayback(bucket.manager, bucket, container, config) - } - View::class.java.isAssignableFrom(playable.config.rendererType) -> { - DynamicViewRendererPlayback(bucket.manager, bucket, container, config) - } - Fragment::class.java.isAssignableFrom(playable.config.rendererType) -> { - DynamicFragmentRendererPlayback(bucket.manager, bucket, container, config) - } - else -> { - throw IllegalArgumentException( - "Unsupported Renderer type: ${playable.config.rendererType}" - ) - } - } - }) - "Request bound: $tag, $container, $playable".logInfo() - } - - internal fun onRemoved() { - "Request removed: $tag, $container, $playable".logWarn() - options.controller = null - options.artworkHintListener = null - options.networkTypeChangeListener = null - options.tokenUpdateListener = null - options.callbacks.clear() - } - - override fun toString(): String { - return "R: $tag, $container" - } - } - // Public APIs /** @@ -781,5 +748,6 @@ class Master private constructor(context: Context) : PlayableManager { /** * Globally unlock the behavior. */ + @Suppress("MemberVisibilityCanBePrivate") fun unlock() = unlock(scope = GLOBAL) } diff --git a/kohii-core/src/main/java/kohii/v1/core/Playable.kt b/kohii-core/src/main/java/kohii/v1/core/Playable.kt index 4be74d76..0457d70c 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Playable.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Playable.kt @@ -59,6 +59,9 @@ abstract class Playable( abstract var renderer: Any? internal set + /** + * The current [Playback] that contains this [Playable], or `null` if there is no binding yet. + */ internal abstract var playback: Playback? internal abstract var manager: PlayableManager? @@ -85,9 +88,9 @@ abstract class Playable( abstract fun onUnbind(playback: Playback) /** - * Return `true` to indicate that this Playable would survive configuration changes and no - * playback reloading would be required. In special cases like YouTube playback, it is recommended - * to return `false` so Kohii will handle the resource recycling correctly. + * Return `true` to indicate that this Playable handles the configuration changes itself, and + * no playback reloading would be required. In special cases like YouTube playback, it is + * recommended to return `false` so the library will handle the resource recycling correctly. */ abstract fun onConfigChange(): Boolean diff --git a/kohii-core/src/main/java/kohii/v1/core/PlayableManager.kt b/kohii-core/src/main/java/kohii/v1/core/PlayableManager.kt new file mode 100644 index 00000000..fdc7b0a8 --- /dev/null +++ b/kohii-core/src/main/java/kohii/v1/core/PlayableManager.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.core + +import kohii.v1.media.PlaybackInfo + +/** + * Common definitions for a class that manages [Playable]s. + * + * @see [Master] + * @see [Manager] + */ +interface PlayableManager { + + /** + * Adds a new [Playable] to this manager. + */ + fun addPlayable(playable: Playable) + + /** + * Removes a [Playable] from this manager. + */ + fun removePlayable(playable: Playable) + + /** + * Saves the [PlaybackInfo] of the [playable]. + */ + fun trySavePlaybackInfo(playable: Playable) + + /** + * Restores the [PlaybackInfo] for the [playable] if it was saved before. + */ + fun tryRestorePlaybackInfo(playable: Playable) +} diff --git a/kohii-core/src/main/java/kohii/v1/core/Playback.kt b/kohii-core/src/main/java/kohii/v1/core/Playback.kt index e0951172..330f4063 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Playback.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Playback.kt @@ -110,6 +110,11 @@ abstract class Playback( } } + /** + * @property releaseOnInActive If `true`, the resources of this [Playback] should be released as + * soon as it is inactive. This flag can be `false` to reduce the releasing/preparing frequency. + * Note that a [Playback] will always be released when it is detached. + */ data class Config( val tag: Any = Master.NO_TAG, val delay: Int = 0, diff --git a/kohii-core/src/main/java/kohii/v1/core/Rebinder.kt b/kohii-core/src/main/java/kohii/v1/core/Rebinder.kt index b23cc2f2..626493fa 100644 --- a/kohii-core/src/main/java/kohii/v1/core/Rebinder.kt +++ b/kohii-core/src/main/java/kohii/v1/core/Rebinder.kt @@ -24,9 +24,9 @@ import kohii.v1.core.Playback.Callback import kohii.v1.core.Playback.Controller import kohii.v1.core.Playback.NetworkTypeChangeListener import kohii.v1.core.Playback.TokenUpdateListener -import kotlinx.android.parcel.IgnoredOnParcel -import kotlinx.android.parcel.Parcelize -import kotlinx.android.parcel.RawValue +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue @Parcelize data class Rebinder(val tag: @RawValue Any) : Parcelable { @@ -60,16 +60,14 @@ data class Rebinder(val tag: @RawValue Any) : Parcelable { engine: Engine<*>, container: ViewGroup, callback: ((Playback) -> Unit)? = null - ) { - this.bind(engine.master, container, callback) - } + ): Unit = this.bind(engine.master, container, callback) private fun bind( master: Master, container: ViewGroup, callback: ((Playback) -> Unit)? = null ) { - val playable = master.playables.asSequence() + val playable: Playable? = master.playables.asSequence() .firstOrNull { it.value == tag /* equals */ } ?.key master.bind( diff --git a/kohii-core/src/main/java/kohii/v1/core/RecycledRendererProvider.kt b/kohii-core/src/main/java/kohii/v1/core/RecycledRendererProvider.kt index af048c8b..3c8c9a9a 100644 --- a/kohii-core/src/main/java/kohii/v1/core/RecycledRendererProvider.kt +++ b/kohii-core/src/main/java/kohii/v1/core/RecycledRendererProvider.kt @@ -57,8 +57,8 @@ abstract class RecycledRendererProvider @JvmOverloads constructor( @CallSuper override fun clear() { - pools.forEach { _, value -> - value.onEachAcquired(::onClear) + pools.forEach { _, pool -> + pool.onEachAcquired(::onClear) } } diff --git a/kohii-core/src/main/java/kohii/v1/internal/BindRequest.kt b/kohii-core/src/main/java/kohii/v1/internal/BindRequest.kt new file mode 100644 index 00000000..57657c85 --- /dev/null +++ b/kohii-core/src/main/java/kohii/v1/internal/BindRequest.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2021 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.internal + +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import kohii.v1.core.Binder.Options +import kohii.v1.core.Bucket +import kohii.v1.core.Master +import kohii.v1.core.Playable +import kohii.v1.core.Playback +import kohii.v1.core.Playback.Config +import kohii.v1.logInfo +import kohii.v1.logWarn + +internal class BindRequest( + val master: Master, + val playable: Playable, + val container: ViewGroup, + val tag: Any, + val options: Options, + val callback: ((Playback) -> Unit)? +) { + + // Used by RecyclerViewBucket to 'assume' that it will hold this container + // It is recommended to use Engine#cancel to easily remove a queued request from cache. + internal var bucket: Bucket? = null + + internal fun onBind() { + val bucket = master.findBucketForContainer(container) + + requireNotNull(bucket) { "No Manager and Bucket available for $container" } + + master.onBind(playable, tag, bucket.manager, container, callback, createNewPlayback@{ + val config = Config( + tag = options.tag, + delay = options.delay, + threshold = options.threshold, + preload = options.preload, + repeatMode = options.repeatMode, + controller = options.controller, + initialPlaybackInfo = options.initialPlaybackInfo, + artworkHintListener = options.artworkHintListener, + tokenUpdateListener = options.tokenUpdateListener, + networkTypeChangeListener = options.networkTypeChangeListener, + callbacks = options.callbacks + ) + + return@createNewPlayback when { + // Scenario: Playable accepts renderer of type PlayerView, and + // the container is an instance of PlayerView or its subtype. + playable.config.rendererType.isAssignableFrom(container.javaClass) -> { + StaticViewRendererPlayback(bucket.manager, bucket, container, config) + } + View::class.java.isAssignableFrom(playable.config.rendererType) -> { + DynamicViewRendererPlayback(bucket.manager, bucket, container, config) + } + Fragment::class.java.isAssignableFrom(playable.config.rendererType) -> { + DynamicFragmentRendererPlayback(bucket.manager, bucket, container, config) + } + else -> { + throw IllegalArgumentException( + "Unsupported Renderer type: ${playable.config.rendererType}" + ) + } + } + }) + "Request#onBind, $this".logInfo() + } + + internal fun onRemoved() { + "Request#onRemoved, $this".logWarn() + options.controller = null + options.artworkHintListener = null + options.networkTypeChangeListener = null + options.tokenUpdateListener = null + options.callbacks.clear() + } + + override fun toString(): String { + return "Request[t=$tag, c=$container, p=$playable]" + } +} diff --git a/kohii-core/src/main/java/kohii/v1/internal/ManagerViewModel.kt b/kohii-core/src/main/java/kohii/v1/internal/ManagerViewModel.kt new file mode 100644 index 00000000..46ddbc41 --- /dev/null +++ b/kohii-core/src/main/java/kohii/v1/internal/ManagerViewModel.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2021 Nam Nguyen, nam@ene.im + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kohii.v1.internal + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel +import kohii.v1.core.Bridge +import kohii.v1.core.Common +import kohii.v1.core.Manager +import kohii.v1.core.Master +import kohii.v1.core.Playable +import kohii.v1.core.Playback +import kohii.v1.debugOnly +import kohii.v1.logDebug +import kohii.v1.logInfo +import kohii.v1.media.PlaybackInfo + +/** + * This [ViewModel] is used to manage the [PlaybackInfo] cache for the [Playback]s the [Manager] is + * managing. It should also be aware of the [Playable]s being used by them. + * + * When the [Manager] is active (its [Manager.lifecycleOwner] is at least + * [Manager.activeLifecycleState]): + * - If a [Playback] is detached, the [PlaybackInfo] of its [Playable] should be saved to this class. + * - If a [Playback] is (re)attached, the [PlaybackInfo] of its [Playable] should be restored from + * this class, and that information is also removed from the cache. + * - If a [Playback] from another [Manager] takes away the [Playable] from a [Playback] managed by + * the [Manager] that owns this class, it should transfer any cached [PlaybackInfo] of that + * [Playable] from this class to the [ManagerViewModel] of that [Manager]. + */ +internal class ManagerViewModel(application: Application) : AndroidViewModel(application) { + + /** + * The [Master]. + */ + private val master = Master[application] + + /** + * A set of [Playable]s those are currently managed by the [Manager] owning this [ViewModel] + */ + private val playables = mutableSetOf() + + /** + * [PlaybackInfo] cache for the [Playable]s managed by this class. + */ + private val playbackInfoStore = mutableMapOf() + + init { + master.onViewModelCreated(this@ManagerViewModel) + } + + internal fun addPlayable(playable: Playable) { + playables.add(playable) + } + + internal fun removePlayable(playable: Playable) { + if (playables.remove(playable)) { + val infoKey = if (playable.tag !== Master.NO_TAG) { + playable.tag + } else { + // if there is no available tag, we use the playback as info key, to allow state caching in + // the same session. + val playback = playable.playback + if (playback == null || !playback.isAttached) return + playback + } + playbackInfoStore.remove(infoKey) + } + } + + internal fun movePlayable(playable: Playable, destination: ManagerViewModel) { + if (playables.contains(playable)) { + tryRestorePlaybackInfo(playable) + removePlayable(playable) + destination.addPlayable(playable) + destination.trySavePlaybackInfo(playable) + } + } + + /** + * @see [Manager.trySavePlaybackInfo] + */ + internal fun trySavePlaybackInfo(playable: Playable) { + if (!playables.contains(playable)) { + debugOnly { + error("Playable $playable is not added to this ViewModel.") + } + return + } + + "ViewModel#trySavePlaybackInfo: $playable".logDebug() + val key = if (playable.tag !== Master.NO_TAG) { + playable.tag + } else { + // If there is no available tag, we use the playback as info key, to allow state caching in + // the same session. + val playback = playable.playback + if (playback == null || !playback.isAttached) return + playback + } + + if (!playbackInfoStore.containsKey(key)) { + val info = playable.playbackInfo + "ViewModel#trySavePlaybackInfo: $info, $playable".logInfo() + playbackInfoStore[key] = info + } + } + + /** + * Note: If this method is called, it must be before any call to [Bridge.prepare]. + * + * @see [Manager.tryRestorePlaybackInfo] + */ + internal fun tryRestorePlaybackInfo(playable: Playable) { + if (!playables.contains(playable)) { + debugOnly { + error("Playable $playable is not added to this ViewModel.") + } + return + } + + "ViewModel#tryRestorePlaybackInfo: $playable".logDebug() + val cache = if (playable.tag !== Master.NO_TAG) { + playbackInfoStore.remove(playable.tag) + } else { + val key = playable.playback ?: return + playbackInfoStore.remove(key) + } + + "ViewModel#tryRestorePlaybackInfo: $cache, $playable".logInfo() + // Only restoring playback state if there is cached state, and the player is not ready yet. + if (cache != null && playable.playerState <= Common.STATE_IDLE) { + playable.playbackInfo = cache + } + } + + override fun onCleared() { + playbackInfoStore.clear() + master.onViewModelCleared(this) + } +} diff --git a/kohii-core/src/main/java/kohii/v1/core/MasterDispatcher.kt b/kohii-core/src/main/java/kohii/v1/internal/MasterDispatcher.kt similarity index 84% rename from kohii-core/src/main/java/kohii/v1/core/MasterDispatcher.kt rename to kohii-core/src/main/java/kohii/v1/internal/MasterDispatcher.kt index 3dad248e..b1d9e050 100644 --- a/kohii-core/src/main/java/kohii/v1/core/MasterDispatcher.kt +++ b/kohii-core/src/main/java/kohii/v1/internal/MasterDispatcher.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Nam Nguyen, nam@ene.im + * Copyright (c) 2021 Nam Nguyen, nam@ene.im * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,13 +14,15 @@ * limitations under the License. */ -package kohii.v1.core +package kohii.v1.internal import android.os.Handler import android.os.Looper import android.os.Message import android.view.ViewGroup import androidx.core.view.doOnAttach +import kohii.v1.core.Master +import kohii.v1.core.Playable import kohii.v1.debugOnly import kohii.v1.logInfo @@ -43,13 +45,10 @@ internal class MasterDispatcher(val master: Master) : Handler(Looper.getMainLoop } } Master.MSG_RELEASE_PLAYABLE -> { - val playable = (msg.obj as Playable) - playable.onRelease() + (msg.obj as Playable).onRelease() } Master.MSG_DESTROY_PLAYABLE -> { - val playable = msg.obj as Playable - val clearState = msg.arg1 == 0 - master.onTearDown(playable, clearState) + master.onTearDown(msg.obj as Playable) } } } diff --git a/kohii-core/src/main/java/kohii/v1/utils/Capsule.kt b/kohii-core/src/main/java/kohii/v1/utils/Capsule.kt index 4d24aa0a..49700b94 100644 --- a/kohii-core/src/main/java/kohii/v1/utils/Capsule.kt +++ b/kohii-core/src/main/java/kohii/v1/utils/Capsule.kt @@ -19,16 +19,16 @@ package kohii.v1.utils import kohii.v1.core.Engine /** - * Singleton Holder + * Singleton Holder. */ open class Capsule( creator: (A) -> T, - onCreate: ((T) -> Unit) = { if (it is Engine<*>) it.master.registerEngine(it) } + onCreated: ((T) -> Unit) = { if (it is Engine<*>) it.master.registerEngine(it) } ) { @Volatile private var instance: T? = null private var creator: ((A) -> T)? = creator - private var onCreate: ((T) -> Unit)? = onCreate + private var onCreated: ((T) -> Unit)? = onCreated protected fun getInstance(arg: A): T { val check = instance @@ -42,10 +42,10 @@ open class Capsule( doubleCheck } else { val created = requireNotNull(creator)(arg) - requireNotNull(onCreate)(created) + requireNotNull(onCreated)(created) instance = created creator = null - onCreate = null + onCreated = null created } } diff --git a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/PlayerViewBridge.kt b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/PlayerViewBridge.kt index 970574ff..921f5905 100644 --- a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/PlayerViewBridge.kt +++ b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/PlayerViewBridge.kt @@ -54,11 +54,10 @@ import kotlin.math.max /** * @author eneim (2018/06/24). */ -@SuppressLint("WrongConstant") +@SuppressLint("WrongConstant", "MemberVisibilityCanBePrivate") open class PlayerViewBridge( context: Context, protected val media: Media, - @Suppress("MemberVisibilityCanBePrivate") protected val playerPool: PlayerPool, mediaSourceFactoryProvider: MediaSourceFactoryProvider ) : AbstractBridge(), PlayerEventListener { diff --git a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/PlayerViewPlayable.kt b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/PlayerViewPlayable.kt index 74fd9eba..45753da6 100644 --- a/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/PlayerViewPlayable.kt +++ b/kohii-exoplayer/src/main/java/kohii/v1/exoplayer/PlayerViewPlayable.kt @@ -21,9 +21,13 @@ import com.google.android.exoplayer2.ui.PlayerView import kohii.v1.core.AbstractPlayable import kohii.v1.core.Bridge import kohii.v1.core.Master +import kohii.v1.core.Playable import kohii.v1.core.Playback import kohii.v1.media.Media +/** + * A [Playable] that can be played in a [PlayerView]. + */ class PlayerViewPlayable( master: Master, media: Media,