Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.lagradost.cloudstream3.actions.VideoClickActionHolder
import com.lagradost.cloudstream3.databinding.ToastBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.ui.BaseFragmentPool
import com.lagradost.cloudstream3.ui.home.HomeChildItemAdapter
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
import com.lagradost.cloudstream3.ui.player.PlayerEventType
Expand Down Expand Up @@ -236,6 +237,7 @@ object CommonActivity {
ioSafe { Torrent.deleteAllFiles() }

// Clear all pools to apply the correct theme
BaseFragmentPool.clearAll()
for (pool in arrayOf(
PluginAdapter.sharedPool, HomeChildItemAdapter.sharedPool,
ParentItemAdapter.sharedPool, ActorAdaptor.sharedPool, EpisodeAdapter.sharedPool,
Expand Down
9 changes: 6 additions & 3 deletions app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.home.HomeFragment
import com.lagradost.cloudstream3.ui.home.HomeViewModel
import com.lagradost.cloudstream3.ui.library.LibraryViewModel
import com.lagradost.cloudstream3.ui.player.BasicLink
Expand Down Expand Up @@ -161,6 +162,7 @@ import com.lagradost.cloudstream3.utils.ImageLoader.loadImage
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar
import com.lagradost.cloudstream3.utils.TvChannelUtils
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
Expand Down Expand Up @@ -194,9 +196,6 @@ import androidx.tvprovider.media.tv.TvContractCompat
import android.content.ComponentName
import android.content.ContentUris

import com.lagradost.cloudstream3.ui.home.HomeFragment
import com.lagradost.cloudstream3.utils.TvChannelUtils

class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback {
companion object {
var activityResultLauncher: ActivityResultLauncher<Intent>? = null
Expand Down Expand Up @@ -2002,11 +2001,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
try {
if (getKey(HAS_DONE_SETUP_KEY, false) != true) {
navController.navigate(R.id.navigation_setup_language)
// Causes cache to be poisoned, so we don't use yet.
HomeFragment.useBindingPool = false
// If no plugins bring up extensions screen
} else if (PluginManager.getPluginsOnline().isEmpty()
&& PluginManager.getPluginsLocal().isEmpty()
// && PREBUILT_REPOSITORIES.isNotEmpty()
) {
// Causes cache to be poisoned, so we don't use yet.
HomeFragment.useBindingPool = false
navController.navigate(
R.id.navigation_setup_extensions,
SetupFragmentExtensions.newInstance(false)
Expand Down
83 changes: 80 additions & 3 deletions app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui

import android.content.res.Configuration
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
Expand Down Expand Up @@ -39,11 +40,22 @@ private interface BaseFragmentHelper<T : ViewBinding> {
var _binding: T?
val binding: T? get() = _binding

companion object {
const val TAG = "BaseFragment"
}

fun createBinding(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Try to reuse a binding from the pool first
BaseFragmentPool.acquire<T>(getPoolKey())?.let {
Log.d(TAG, "Binding acquired from pool for ${getPoolKey()}")
_binding = it
return it.root
}

val layoutId = pickLayout()
val root: View? = layoutId?.let { inflater.inflate(it, container, false) }
_binding = try {
Expand All @@ -66,6 +78,8 @@ private interface BaseFragmentHelper<T : ViewBinding> {
return _binding?.root ?: root
}

fun getPoolKey(): String = javaClass.name

/**
* Called after the fragment's view has been created.
*
Expand Down Expand Up @@ -119,6 +133,69 @@ private interface BaseFragmentHelper<T : ViewBinding> {
* @param view The root view to adjust.
*/
fun fixLayout(view: View)

/** Called by fragments when they’re destroyed, so the binding can be recycled. */
fun recycleBindingOnDestroy() {
_binding?.let {
BaseFragmentPool.release(getPoolKey(), it)
Log.d(TAG, "Binding released to pool for ${getPoolKey()}")
_binding = null
}
}
}

/**
* A global pool for reusing [ViewBinding] instances across fragments to reduce
* layout inflation overhead and improve navigation performance.
*
* This pool is intended for use with fragments that extend [BaseFragment],
* [BaseDialogFragment], or [BaseBottomSheetDialogFragment] which support
* recycling of their bindings.
*/
object BaseFragmentPool {
private val pool = mutableMapOf<String, MutableList<ViewBinding>>()
private const val MAX_PER_PREFIX = 3

/** Attempts to acquire a recycled binding from the pool. */
fun <T : ViewBinding> acquire(key: String): T? {
if (key == "") return null
val list = pool[key] ?: return null
@Suppress("UNCHECKED_CAST")
val binding = list.removeLastOrNull() as? T ?: return null
(binding.root.parent as? ViewGroup)?.removeView(binding.root)
if (list.isEmpty()) pool.remove(key)
return binding
}

/** Releases a binding back to the pool for later reuse. */
fun <T : ViewBinding> release(key: String, binding: T) {
if (key == "") return
val list = pool.getOrPut(key) { mutableListOf() }
list.add(binding)
trimPrefixIfNeeded(key)
}

/** Clears all cached bindings from the pool. */
fun clearAll() {
pool.values.flatten().forEach { (it.root.parent as? ViewGroup)?.removeView(it.root) }
pool.clear()
}

/** Trims bindings for a prefix if total exceeds MAX_PER_PREFIX */
private fun trimPrefixIfNeeded(key: String) {
val prefix = key.substringBefore(":")
val prefixKeys = pool.keys.filter { it.startsWith("$prefix:") }
var total = prefixKeys.sumOf { pool[it]?.size ?: 0 }

while (total > MAX_PER_PREFIX && prefixKeys.isNotEmpty()) {
// Remove oldest from the first key with items
val oldestKey = prefixKeys.firstOrNull { pool[it]?.isNotEmpty() == true } ?: break
val removed = pool[oldestKey]?.removeFirstOrNull() ?: break
(removed.root.parent as? ViewGroup)?.removeView(removed.root)
if (pool[oldestKey]?.isEmpty() == true) pool.remove(oldestKey)
total--
}
}
}

abstract class BaseFragment<T : ViewBinding>(
Expand Down Expand Up @@ -150,7 +227,7 @@ abstract class BaseFragment<T : ViewBinding>(
/** Cleans up the binding reference when the view is destroyed to avoid memory leaks. */
override fun onDestroyView() {
super.onDestroyView()
_binding = null
recycleBindingOnDestroy()
}

/**
Expand Down Expand Up @@ -205,7 +282,7 @@ abstract class BaseDialogFragment<T : ViewBinding>(
/** Cleans up the binding reference when the view is destroyed to avoid memory leaks. */
override fun onDestroyView() {
super.onDestroyView()
_binding = null
recycleBindingOnDestroy()
}
}

Expand Down Expand Up @@ -234,7 +311,7 @@ abstract class BaseBottomSheetDialogFragment<T : ViewBinding>(
/** Cleans up the binding reference when the view is destroyed to avoid memory leaks. */
override fun onDestroyView() {
super.onDestroyView()
_binding = null
recycleBindingOnDestroy()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(
BaseFragment.BindingCreator.Bind(FragmentHomeBinding::bind)
) {
companion object {
val configEvent = Event<Int>()
var currentSpan = 1
var useBindingPool = true
val configEvent = Event<Int>()
val listHomepageItems = mutableListOf<SearchResponse>()

private val errorProfilePics = listOf(
Expand Down Expand Up @@ -553,6 +554,11 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(
override fun pickLayout(): Int? =
if (isLayout(PHONE)) R.layout.fragment_home else R.layout.fragment_home_tv

override fun getPoolKey(): String {
if (!useBindingPool) return ""
return "HomeFragment"
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ class ResultFragmentTv : BaseFragment<FragmentResultTvBinding>(
return super.onCreateView(inflater, container, savedInstanceState)
}

override fun getPoolKey(): String {
// Prevent poisoned pool by using no cache if we can't get key for it
val storedData = getStoredData() ?: return ""
return "ResultFragmentTv:${storedData.name}-${storedData.apiName}"
}

private fun updateUI(id: Int?) {
viewModel.reloadEpisodes()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class SetupFragmentExtensions : BaseFragment<FragmentSetupExtensionsBinding>(
fixSystemBarsPadding(view)
}

// No cache, it should not be shown very often,
// and it just adds to memory usage.
override fun getPoolKey(): String = ""

private fun setRepositories(success: Boolean = true) {
main {
val repositories = RepositoryManager.getRepositories() + PREBUILT_REPOSITORIES
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class SetupFragmentLanguage : BaseFragment<FragmentSetupLanguageBinding>(
fixSystemBarsPadding(view)
}

// No cache, it should not be shown very often,
// and it just adds to memory usage.
override fun getPoolKey(): String = ""

override fun onBindingCreated(binding: FragmentSetupLanguageBinding) {
// We don't want a crash for all users
safe {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class SetupFragmentLayout : BaseFragment<FragmentSetupLayoutBinding>(
fixSystemBarsPadding(view)
}

// No cache, it should not be shown very often,
// and it just adds to memory usage.
override fun getPoolKey(): String = ""

override fun onBindingCreated(binding: FragmentSetupLayoutBinding) {
safe {
val ctx = context ?: return@safe
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class SetupFragmentMedia : BaseFragment<FragmentSetupMediaBinding>(
fixSystemBarsPadding(view)
}

// No cache, it should not be shown very often,
// and it just adds to memory usage.
override fun getPoolKey(): String = ""

override fun onBindingCreated(binding: FragmentSetupMediaBinding) {
safe {
val ctx = context ?: return@safe
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ class SetupFragmentProviderLanguage : BaseFragment<FragmentSetupProviderLanguage
fixSystemBarsPadding(view)
}

// No cache, it should not be shown very often,
// and it just adds to memory usage.
override fun getPoolKey(): String = ""

override fun onBindingCreated(binding: FragmentSetupProviderLanguagesBinding) {
safe {
val ctx = context ?: return@safe
Expand Down