From f270f9f55170d1c147bc7faa1e3ba5bc16f8cf15 Mon Sep 17 00:00:00 2001 From: LagradOst Date: Wed, 28 Jul 2021 21:14:45 +0200 Subject: [PATCH] fixed mem leak --- app/build.gradle | 2 +- .../ui/download/DownloadButtonSetup.kt | 174 ---------------- .../ui/download/DownloadButtonViewHolder.kt | 6 + .../ui/download/DownloadChildAdapter.kt | 69 +++++-- .../ui/download/DownloadChildFragment.kt | 16 +- .../ui/download/DownloadFragment.kt | 28 ++- .../ui/download/DownloadHeaderAdapter.kt | 65 +++++- .../ui/download/EasyDownloadButton.kt | 188 ++++++++++++++++++ .../cloudstream3/ui/result/EpisodeAdapter.kt | 71 +++++-- .../cloudstream3/ui/result/ResultFragment.kt | 17 +- .../cloudstream3/ui/search/SearchFragment.kt | 6 +- .../cloudstream3/utils/IDisposable.kt | 12 ++ 12 files changed, 427 insertions(+), 227 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonViewHolder.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/IDisposable.kt diff --git a/app/build.gradle b/app/build.gradle index d3dd0001..3a06c023 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,8 +1,8 @@ plugins { id 'com.android.application' id 'kotlin-android' - id 'kotlin-android-extensions' id 'kotlin-kapt' + id 'kotlin-android-extensions' } android { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 26235c8c..357c69ab 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -1,24 +1,13 @@ package com.lagradost.cloudstream3.ui.download -import android.animation.ObjectAnimator -import android.annotation.SuppressLint import android.app.Activity import android.content.DialogInterface -import android.view.View -import android.view.animation.DecelerateInterpolator -import android.widget.ImageView -import android.widget.TextView import androidx.appcompat.app.AlertDialog -import androidx.core.widget.ContentLoadingProgressBar import androidx.fragment.app.FragmentActivity -import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.ui.player.PlayerFragment import com.lagradost.cloudstream3.ui.player.UriData -import com.lagradost.cloudstream3.utils.Coroutines import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager object DownloadButtonSetup { @@ -95,167 +84,4 @@ object DownloadButtonSetup { } } } - - fun setUpDownloadButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - textView: TextView?, - data: VideoDownloadHelper.DownloadEpisodeCached, - downloadView: View, - downloadImageChangeCallback: (Pair) -> Unit, - clickCallback: (DownloadClickEvent) -> Unit, - ): () -> Unit { - var lastState: VideoDownloadManager.DownloadType? = null - var currentBytes = setupCurrentBytes ?: 0 - var totalBytes = setupTotalBytes ?: 0 - var needImageUpdate = false - - fun changeDownloadImage(state: VideoDownloadManager.DownloadType) { - lastState = state - if (currentBytes <= 0) needImageUpdate = true - val img = if (currentBytes > 0) { - when (state) { - VideoDownloadManager.DownloadType.IsPaused -> Pair( - R.drawable.ic_baseline_play_arrow_24, - "Download Paused" - ) - VideoDownloadManager.DownloadType.IsDownloading -> Pair(R.drawable.netflix_pause, "Downloading") - else -> Pair(R.drawable.ic_baseline_delete_outline_24, "Downloaded") - } - } else { - Pair(R.drawable.netflix_download, "Download") - } - downloadImageChangeCallback.invoke(img) - } - - @SuppressLint("SetTextI18n") - fun fixDownloadedBytes(setCurrentBytes: Long, setTotalBytes: Long, animate: Boolean) { - currentBytes = setCurrentBytes - totalBytes = setTotalBytes - - if (currentBytes == 0L) { - changeDownloadImage(VideoDownloadManager.DownloadType.IsStopped) - textView?.visibility = View.GONE - progressBar?.visibility = View.GONE - } else { - if (lastState == VideoDownloadManager.DownloadType.IsStopped) { - changeDownloadImage(VideoDownloadManager.getDownloadState(data.id)) - } - textView?.visibility = View.VISIBLE - progressBar?.visibility = View.VISIBLE - val currentMbString = "%.1f".format(setCurrentBytes / 1000000f) - val totalMbString = "%.1f".format(setTotalBytes / 1000000f) - - textView?.text = - "${currentMbString}MB / ${totalMbString}MB" - - progressBar?.let { bar -> - bar.max = (setTotalBytes / 1000).toInt() - - if (animate) { - val animation: ObjectAnimator = ObjectAnimator.ofInt( - bar, - "progress", - bar.progress, - (setCurrentBytes / 1000).toInt() - ) - animation.duration = 500 - animation.setAutoCancel(true) - animation.interpolator = DecelerateInterpolator() - animation.start() - } else { - bar.progress = (setCurrentBytes / 1000).toInt() - } - } - } - } - - fixDownloadedBytes(currentBytes, totalBytes, false) - changeDownloadImage(VideoDownloadManager.getDownloadState(data.id)) - - val downloadProgressEventListener = { downloadData: Triple -> - if (data.id == downloadData.first) { - if (downloadData.second != currentBytes || downloadData.third != totalBytes) { // TO PREVENT WASTING UI TIME - Coroutines.runOnMainThread { - fixDownloadedBytes(downloadData.second, downloadData.third, true) - } - } - } - } - - val downloadStatusEventListener = { downloadData: Pair -> - if (data.id == downloadData.first) { - if (lastState != downloadData.second || needImageUpdate) { // TO PREVENT WASTING UI TIME - Coroutines.runOnMainThread { - changeDownloadImage(downloadData.second) - } - } - } - } - - VideoDownloadManager.downloadProgressEvent += downloadProgressEventListener - VideoDownloadManager.downloadStatusEvent += downloadStatusEventListener - - downloadView.setOnClickListener { - if (currentBytes <= 0) { - clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) - } else { - val list = arrayListOf( - Pair(DOWNLOAD_ACTION_PLAY_FILE, R.string.popup_play_file), - Pair(DOWNLOAD_ACTION_DELETE_FILE, R.string.popup_delete_file), - ) - - // DON'T RESUME A DOWNLOADED FILE lastState != VideoDownloadManager.DownloadType.IsDone && - if ((currentBytes * 100 / totalBytes) < 98) { - list.add( - if (lastState == VideoDownloadManager.DownloadType.IsDownloading) - Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download) - else - Pair(DOWNLOAD_ACTION_RESUME_DOWNLOAD, R.string.popup_resume_download) - ) - } - - it.popupMenuNoIcons( - list - ) { - clickCallback.invoke(DownloadClickEvent(itemId, data)) - } - } - } - - return { - VideoDownloadManager.downloadProgressEvent -= downloadProgressEventListener - VideoDownloadManager.downloadStatusEvent -= downloadStatusEventListener - } - } - - fun setUpMaterialButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - downloadButton: MaterialButton, - textView: TextView?, - data: VideoDownloadHelper.DownloadEpisodeCached, - clickCallback: (DownloadClickEvent) -> Unit, - ): () -> Unit { - return setUpDownloadButton(setupCurrentBytes, setupTotalBytes, progressBar, textView, data, downloadButton, { - downloadButton?.setIconResource(it.first) - downloadButton?.text = it.second - }, clickCallback) - } - - fun setUpButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - downloadImage: ImageView, - textView: TextView?, - data: VideoDownloadHelper.DownloadEpisodeCached, - clickCallback: (DownloadClickEvent) -> Unit, - ): () -> Unit { - return setUpDownloadButton(setupCurrentBytes, setupTotalBytes, progressBar, textView, data, downloadImage, { - downloadImage?.setImageResource(it.first) - }, clickCallback) - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonViewHolder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonViewHolder.kt new file mode 100644 index 00000000..0096ff42 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonViewHolder.kt @@ -0,0 +1,6 @@ +package com.lagradost.cloudstream3.ui.download + +interface DownloadButtonViewHolder { + var downloadButton : EasyDownloadButton + fun reattachDownloadButton() +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt index a360b9e9..87b00bed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt @@ -1,28 +1,20 @@ package com.lagradost.cloudstream3.ui.download -import android.animation.ObjectAnimator import android.annotation.SuppressLint -import android.os.Handler -import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.animation.DecelerateInterpolator import android.widget.ImageView -import android.widget.ProgressBar import android.widget.TextView import androidx.cardview.widget.CardView import androidx.core.widget.ContentLoadingProgressBar import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.UIHelper.popupMenuNoIcons -import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.VideoDownloadHelper -import com.lagradost.cloudstream3.utils.VideoDownloadManager -import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadState import kotlinx.android.synthetic.main.download_child_episode.view.* +import java.util.* const val DOWNLOAD_ACTION_PLAY_FILE = 0 const val DOWNLOAD_ACTION_DELETE_FILE = 1 @@ -41,8 +33,37 @@ data class DownloadClickEvent(val action: Int, val data: VideoDownloadHelper.Dow class DownloadChildAdapter( var cardList: List, private val clickCallback: (DownloadClickEvent) -> Unit, -) : - RecyclerView.Adapter() { +) : RecyclerView.Adapter() { + + private val mBoundViewHolders: HashSet = HashSet() + private fun getAllBoundViewHolders(): Set? { + return Collections.unmodifiableSet(mBoundViewHolders) + } + + fun killAdapter() { + getAllBoundViewHolders()?.forEach { view -> + view?.downloadButton?.dispose() + } + } + + override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { + if (holder is DownloadButtonViewHolder) { + holder.downloadButton.dispose() + } + } + + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + if (holder is DownloadButtonViewHolder) { + holder.downloadButton.dispose() + mBoundViewHolders.remove(holder); + } + } + + override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { + if (holder is DownloadButtonViewHolder) { + holder.reattachDownloadButton() + } + } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return DownloadChildViewHolder( @@ -55,6 +76,7 @@ class DownloadChildAdapter( when (holder) { is DownloadChildViewHolder -> { holder.bind(cardList[position]) + mBoundViewHolders.add(holder) } } } @@ -67,7 +89,9 @@ class DownloadChildAdapter( constructor( itemView: View, private val clickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(itemView) { + ) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder { + override var downloadButton = EasyDownloadButton() + private val title: TextView = itemView.download_child_episode_text private val extraInfo: TextView = itemView.download_child_episode_text_extra private val holder: CardView = itemView.download_child_episode_holder @@ -75,8 +99,11 @@ class DownloadChildAdapter( private val progressBarDownload: ContentLoadingProgressBar = itemView.download_child_episode_progress_downloaded private val downloadImage: ImageView = itemView.download_child_episode_download + var localCard : VisualDownloadChildCached? = null + @SuppressLint("SetTextI18n") fun bind(card: VisualDownloadChildCached) { + localCard = card val d = card.data val posDur = itemView.context.getViewPos(d.id) @@ -90,7 +117,7 @@ class DownloadChildAdapter( } title.text = d.name ?: "Episode ${d.episode}" //TODO FIX - DownloadButtonSetup.setUpButton( + downloadButton.setUpButton( card.currentBytes, card.totalBytes, progressBarDownload, @@ -104,5 +131,21 @@ class DownloadChildAdapter( clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) } } + + override fun reattachDownloadButton() { + downloadButton.dispose() + val card = localCard + if (card != null) { + downloadButton.setUpButton( + card.currentBytes, + card.totalBytes, + progressBarDownload, + downloadImage, + extraInfo, + card.data, + clickCallback + ) + } + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index 2115f4b8..9d4e5d1c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -30,6 +30,16 @@ class DownloadChildFragment : Fragment() { } } + override fun onDestroyView() { + (download_child_list?.adapter as DownloadChildAdapter?)?.killAdapter() + super.onDestroyView() + } + + override fun onDestroy() { + downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } + super.onDestroy() + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_child_downloads, container, false) } @@ -58,6 +68,8 @@ class DownloadChildFragment : Fragment() { download_child_list?.adapter?.notifyDataSetChanged() } + var downloadDeleteEventListener: ((Int) -> Unit)? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -82,7 +94,7 @@ class DownloadChildFragment : Fragment() { handleDownloadClick(activity, name, click) } - VideoDownloadManager.downloadDeleteEvent += { id -> + downloadDeleteEventListener = { id: Int -> val list = (download_child_list?.adapter as DownloadChildAdapter?)?.cardList if (list != null) { if (list.any { it.data.id == id }) { @@ -91,6 +103,8 @@ class DownloadChildFragment : Fragment() { } } + downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + download_child_list.adapter = adapter download_child_list.layoutManager = GridLayoutManager(context, 1) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index a54afc73..1e7aba9d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -11,19 +11,15 @@ import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.isMovieType import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick -import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE import com.lagradost.cloudstream3.utils.DataStore.getFolderName import com.lagradost.cloudstream3.utils.VideoDownloadManager -import kotlinx.android.synthetic.main.fragment_child_downloads.* import kotlinx.android.synthetic.main.fragment_downloads.* -import kotlinx.android.synthetic.main.fragment_result.* class DownloadFragment : Fragment() { private lateinit var downloadsViewModel: DownloadViewModel @@ -41,9 +37,19 @@ class DownloadFragment : Fragment() { this.layoutParams = param } - fun setList(list : List) { - (download_list?.adapter as DownloadHeaderAdapter? ?: return).cardList = list - (download_list?.adapter as DownloadHeaderAdapter? ?: return).notifyDataSetChanged() + private fun setList(list: List) { + (download_list?.adapter as DownloadHeaderAdapter?)?.cardList = list + download_list?.adapter?.notifyDataSetChanged() + } + + override fun onDestroyView() { + (download_list?.adapter as DownloadHeaderAdapter?)?.killAdapter() + super.onDestroyView() + } + + override fun onDestroy() { + downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } + super.onDestroy() } @SuppressLint("SetTextI18n") @@ -76,6 +82,8 @@ class DownloadFragment : Fragment() { return inflater.inflate(R.layout.fragment_downloads, container, false) } + var downloadDeleteEventListener: ((Int) -> Unit)? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val adapter: RecyclerView.Adapter = @@ -95,13 +103,13 @@ class DownloadFragment : Fragment() { }, { downloadClickEvent -> handleDownloadClick(activity, downloadClickEvent.data.name, downloadClickEvent) - if(downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { + if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { downloadsViewModel.updateList(requireContext()) } } ) - VideoDownloadManager.downloadDeleteEvent += { id -> + downloadDeleteEventListener = { id -> val list = (download_list?.adapter as DownloadHeaderAdapter?)?.cardList if (list != null) { if (list.any { it.data.id == id }) { @@ -111,6 +119,8 @@ class DownloadFragment : Fragment() { } } + downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + download_list.adapter = adapter download_list.layoutManager = GridLayoutManager(context, 1) downloadsViewModel.updateList(requireContext()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt index aa2c8df6..2bb63070 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt @@ -12,10 +12,10 @@ import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.bumptech.glide.load.model.GlideUrl import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.isMovieType -import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.setUpButton +import com.lagradost.cloudstream3.utils.IDisposable import com.lagradost.cloudstream3.utils.VideoDownloadHelper import kotlinx.android.synthetic.main.download_header_episode.view.* +import java.util.* data class VisualDownloadHeaderCached( val currentOngoingDownloads: Int, @@ -32,8 +32,37 @@ class DownloadHeaderAdapter( var cardList: List, private val clickCallback: (DownloadHeaderClickEvent) -> Unit, private val movieClickCallback: (DownloadClickEvent) -> Unit, -) : - RecyclerView.Adapter() { +) : RecyclerView.Adapter() { + + private val mBoundViewHolders: HashSet = HashSet() + private fun getAllBoundViewHolders(): Set? { + return Collections.unmodifiableSet(mBoundViewHolders) + } + + fun killAdapter() { + getAllBoundViewHolders()?.forEach { view -> + view?.downloadButton?.dispose() + } + } + + override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { + if (holder is DownloadButtonViewHolder) { + holder.downloadButton.dispose() + } + } + + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + if (holder is DownloadButtonViewHolder) { + holder.downloadButton.dispose() + mBoundViewHolders.remove(holder); + } + } + + override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { + if (holder is DownloadButtonViewHolder) { + holder.reattachDownloadButton() + } + } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return DownloadHeaderViewHolder( @@ -47,6 +76,7 @@ class DownloadHeaderAdapter( when (holder) { is DownloadHeaderViewHolder -> { holder.bind(cardList[position]) + mBoundViewHolders.add(holder) } } } @@ -60,7 +90,9 @@ class DownloadHeaderAdapter( itemView: View, private val clickCallback: (DownloadHeaderClickEvent) -> Unit, private val movieClickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(itemView) { + ) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder { + override var downloadButton = EasyDownloadButton() + private val poster: ImageView = itemView.download_header_poster private val title: TextView = itemView.download_header_title private val extraInfo: TextView = itemView.download_header_info @@ -69,9 +101,11 @@ class DownloadHeaderAdapter( private val downloadBar: ContentLoadingProgressBar = itemView.download_header_progress_downloaded private val downloadImage: ImageView = itemView.download_header_episode_download private val normalImage: ImageView = itemView.download_header_goto_child + var localCard: VisualDownloadHeaderCached? = null @SuppressLint("SetTextI18n") fun bind(card: VisualDownloadHeaderCached) { + localCard = card val d = card.data if (d.poster != null) { @@ -93,8 +127,7 @@ class DownloadHeaderAdapter( downloadBar.visibility = View.VISIBLE downloadImage.visibility = View.VISIBLE normalImage.visibility = View.GONE - - setUpButton( + /*setUpButton( card.currentBytes, card.totalBytes, downloadBar, @@ -102,7 +135,7 @@ class DownloadHeaderAdapter( extraInfo, card.child, movieClickCallback - ) + )*/ holder.setOnClickListener { movieClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child)) @@ -120,5 +153,21 @@ class DownloadHeaderAdapter( } } } + + override fun reattachDownloadButton() { + downloadButton.dispose() + val card = localCard + if (card?.child != null) { + downloadButton.setUpButton( + card.currentBytes, + card.totalBytes, + downloadBar, + downloadImage, + extraInfo, + card.child, + movieClickCallback + ) + } + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt new file mode 100644 index 00000000..f1bc7e88 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt @@ -0,0 +1,188 @@ +package com.lagradost.cloudstream3.ui.download + +import android.animation.ObjectAnimator +import android.annotation.SuppressLint +import android.view.View +import android.view.animation.DecelerateInterpolator +import android.widget.ImageView +import android.widget.TextView +import androidx.core.widget.ContentLoadingProgressBar +import com.google.android.material.button.MaterialButton +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.UIHelper.popupMenuNoIcons +import com.lagradost.cloudstream3.utils.Coroutines +import com.lagradost.cloudstream3.utils.IDisposable +import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import com.lagradost.cloudstream3.utils.VideoDownloadManager + +class EasyDownloadButton : IDisposable { + override fun dispose() { + try { + downloadProgressEventListener?.let { VideoDownloadManager.downloadProgressEvent -= it } + downloadStatusEventListener?.let { VideoDownloadManager.downloadStatusEvent -= it } + } catch (e: Exception) { + e.printStackTrace() + } + } + + var downloadProgressEventListener: ((Triple) -> Unit)? = null + var downloadStatusEventListener: ((Pair) -> Unit)? = null + + fun setUpMaterialButton( + setupCurrentBytes: Long?, + setupTotalBytes: Long?, + progressBar: ContentLoadingProgressBar, + downloadButton: MaterialButton, + textView: TextView?, + data: VideoDownloadHelper.DownloadEpisodeCached, + clickCallback: (DownloadClickEvent) -> Unit, + ) { + setUpDownloadButton(setupCurrentBytes, setupTotalBytes, progressBar, textView, data, downloadButton, { + downloadButton?.setIconResource(it.first) + downloadButton?.text = it.second + }, clickCallback) + } + + fun setUpButton( + setupCurrentBytes: Long?, + setupTotalBytes: Long?, + progressBar: ContentLoadingProgressBar, + downloadImage: ImageView, + textView: TextView?, + data: VideoDownloadHelper.DownloadEpisodeCached, + clickCallback: (DownloadClickEvent) -> Unit, + ) { + setUpDownloadButton(setupCurrentBytes, setupTotalBytes, progressBar, textView, data, downloadImage, { + downloadImage?.setImageResource(it.first) + }, clickCallback) + } + + fun setUpDownloadButton( + setupCurrentBytes: Long?, + setupTotalBytes: Long?, + progressBar: ContentLoadingProgressBar, + textView: TextView?, + data: VideoDownloadHelper.DownloadEpisodeCached, + downloadView: View, + downloadImageChangeCallback: (Pair) -> Unit, + clickCallback: (DownloadClickEvent) -> Unit, + ) { + var lastState: VideoDownloadManager.DownloadType? = null + var currentBytes = setupCurrentBytes ?: 0 + var totalBytes = setupTotalBytes ?: 0 + var needImageUpdate = false + + fun changeDownloadImage(state: VideoDownloadManager.DownloadType) { + lastState = state + if (currentBytes <= 0) needImageUpdate = true + val img = if (currentBytes > 0) { + when (state) { + VideoDownloadManager.DownloadType.IsPaused -> Pair( + R.drawable.ic_baseline_play_arrow_24, + "Download Paused" + ) + VideoDownloadManager.DownloadType.IsDownloading -> Pair(R.drawable.netflix_pause, "Downloading") + else -> Pair(R.drawable.ic_baseline_delete_outline_24, "Downloaded") + } + } else { + Pair(R.drawable.netflix_download, "Download") + } + downloadImageChangeCallback.invoke(img) + } + + @SuppressLint("SetTextI18n") + fun fixDownloadedBytes(setCurrentBytes: Long, setTotalBytes: Long, animate: Boolean) { + currentBytes = setCurrentBytes + totalBytes = setTotalBytes + + if (currentBytes == 0L) { + changeDownloadImage(VideoDownloadManager.DownloadType.IsStopped) + textView?.visibility = View.GONE + progressBar?.visibility = View.GONE + } else { + if (lastState == VideoDownloadManager.DownloadType.IsStopped) { + changeDownloadImage(VideoDownloadManager.getDownloadState(data.id)) + } + textView?.visibility = View.VISIBLE + progressBar?.visibility = View.VISIBLE + val currentMbString = "%.1f".format(setCurrentBytes / 1000000f) + val totalMbString = "%.1f".format(setTotalBytes / 1000000f) + + textView?.text = + "${currentMbString}MB / ${totalMbString}MB" + + progressBar?.let { bar -> + bar.max = (setTotalBytes / 1000).toInt() + + if (animate) { + val animation: ObjectAnimator = ObjectAnimator.ofInt( + bar, + "progress", + bar.progress, + (setCurrentBytes / 1000).toInt() + ) + animation.duration = 500 + animation.setAutoCancel(true) + animation.interpolator = DecelerateInterpolator() + animation.start() + } else { + bar.progress = (setCurrentBytes / 1000).toInt() + } + } + } + } + + fixDownloadedBytes(currentBytes, totalBytes, false) + changeDownloadImage(VideoDownloadManager.getDownloadState(data.id)) + + downloadProgressEventListener = { downloadData: Triple -> + if (data.id == downloadData.first) { + if (downloadData.second != currentBytes || downloadData.third != totalBytes) { // TO PREVENT WASTING UI TIME + Coroutines.runOnMainThread { + fixDownloadedBytes(downloadData.second, downloadData.third, true) + } + } + } + } + + downloadStatusEventListener = { downloadData: Pair -> + if (data.id == downloadData.first) { + if (lastState != downloadData.second || needImageUpdate) { // TO PREVENT WASTING UI TIME + Coroutines.runOnMainThread { + changeDownloadImage(downloadData.second) + } + } + } + } + + downloadProgressEventListener?.let { VideoDownloadManager.downloadProgressEvent += it } + downloadStatusEventListener?.let { VideoDownloadManager.downloadStatusEvent += it } + + downloadView.setOnClickListener { + if (currentBytes <= 0) { + clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) + } else { + val list = arrayListOf( + Pair(DOWNLOAD_ACTION_PLAY_FILE, R.string.popup_play_file), + Pair(DOWNLOAD_ACTION_DELETE_FILE, R.string.popup_delete_file), + ) + + // DON'T RESUME A DOWNLOADED FILE lastState != VideoDownloadManager.DownloadType.IsDone && + if ((currentBytes * 100 / totalBytes) < 98) { + list.add( + if (lastState == VideoDownloadManager.DownloadType.IsDownloading) + Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download) + else + Pair(DOWNLOAD_ACTION_RESUME_DOWNLOAD, R.string.popup_resume_download) + ) + } + + it.popupMenuNoIcons( + list + ) { + clickCallback.invoke(DownloadClickEvent(itemId, data)) + } + } + } + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index ce2bcdf4..f7536dee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -13,15 +13,15 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.model.GlideUrl import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD -import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup -import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick +import com.lagradost.cloudstream3.ui.download.DownloadButtonViewHolder import com.lagradost.cloudstream3.ui.download.DownloadClickEvent -import com.lagradost.cloudstream3.utils.Event +import com.lagradost.cloudstream3.ui.download.EasyDownloadButton import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import kotlinx.android.synthetic.main.result_episode.view.episode_holder import kotlinx.android.synthetic.main.result_episode.view.episode_text import kotlinx.android.synthetic.main.result_episode_large.view.* +import java.util.* const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 const val ACTION_PLAY_EPISODE_IN_VLC_PLAYER = 2 @@ -49,12 +49,43 @@ class EpisodeAdapter( private val downloadClickCallback: (DownloadClickEvent) -> Unit, ) : RecyclerView.Adapter() { + private val mBoundViewHolders: HashSet = HashSet() + private fun getAllBoundViewHolders(): Set? { + return Collections.unmodifiableSet(mBoundViewHolders) + } + + fun killAdapter() { + getAllBoundViewHolders()?.forEach { view -> + view?.downloadButton?.dispose() + } + } + + override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { + if (holder is DownloadButtonViewHolder) { + holder.downloadButton.dispose() + } + } + + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + if (holder is DownloadButtonViewHolder) { + holder.downloadButton.dispose() + mBoundViewHolders.remove(holder); + } + } + + override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { + if (holder is DownloadButtonViewHolder) { + holder.reattachDownloadButton() + } + } + @LayoutRes private var layout: Int = 0 fun updateLayout() { - layout = if (cardList.filter { it.poster != null }.size >= cardList.size / 2f) // If over half has posters then use the large layout - R.layout.result_episode_large - else R.layout.result_episode + layout = + if (cardList.filter { it.poster != null }.size >= cardList.size / 2f) // If over half has posters then use the large layout + R.layout.result_episode_large + else R.layout.result_episode } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { @@ -62,7 +93,7 @@ class EpisodeAdapter( R.layout.result_episode_large else R.layout.result_episode*/ - return CardViewHolder( + return EpisodeCardViewHolder( LayoutInflater.from(parent.context).inflate(layout, parent, false), hasDownloadSupport, clickCallback, @@ -72,8 +103,9 @@ class EpisodeAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { - is CardViewHolder -> { + is EpisodeCardViewHolder -> { holder.bind(cardList[position]) + mBoundViewHolders.add(holder) } } } @@ -82,26 +114,31 @@ class EpisodeAdapter( return cardList.size } - class CardViewHolder + class EpisodeCardViewHolder constructor( itemView: View, - private val hasDownloadSupport: Boolean, + val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(itemView) { + ) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder { + override var downloadButton = EasyDownloadButton() + private val episodeText: TextView = itemView.episode_text private val episodeRating: TextView? = itemView.episode_rating private val episodeDescript: TextView? = itemView.episode_descript private val episodeProgress: ContentLoadingProgressBar? = itemView.episode_progress private val episodePoster: ImageView? = itemView.episode_poster - private val episodeDownloadBar: ContentLoadingProgressBar = itemView.result_episode_progress_downloaded - private val episodeDownloadImage: ImageView = itemView.result_episode_download + val episodeDownloadBar: ContentLoadingProgressBar = itemView.result_episode_progress_downloaded + val episodeDownloadImage: ImageView = itemView.result_episode_download private val episodeHolder = itemView.episode_holder + var localCard: ResultEpisode? = null + @SuppressLint("SetTextI18n") fun bind(card: ResultEpisode) { + localCard = card val name = if (card.name == null) "Episode ${card.episode}" else "${card.episode}. ${card.name}" episodeText.text = name @@ -148,11 +185,15 @@ class EpisodeAdapter( episodeDownloadImage.visibility = if (hasDownloadSupport) View.VISIBLE else View.GONE episodeDownloadBar.visibility = if (hasDownloadSupport) View.VISIBLE else View.GONE + } - if (hasDownloadSupport) { + override fun reattachDownloadButton() { + downloadButton.dispose() + val card = localCard + if (hasDownloadSupport && card != null) { val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(itemView.context, card.id) - DownloadButtonSetup.setUpButton( + downloadButton.setUpButton( downloadInfo?.fileLength, downloadInfo?.totalBytes, episodeDownloadBar, episodeDownloadImage, null, VideoDownloadHelper.DownloadEpisodeCached( card.name, card.poster, card.episode, card.season, card.id, 0, card.rating, card.descript diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 8c7d2ec5..c6644dd6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -50,8 +50,9 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD +import com.lagradost.cloudstream3.ui.download.EasyDownloadButton +import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick -import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.setUpMaterialButton import com.lagradost.cloudstream3.ui.player.PlayerData import com.lagradost.cloudstream3.ui.player.PlayerFragment import com.lagradost.cloudstream3.utils.* @@ -70,7 +71,6 @@ import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap - const val MAX_SYNO_LENGH = 300 data class ResultEpisode( @@ -156,6 +156,7 @@ class ResultFragment : Fragment() { private var currentHeaderName: String? = null private var currentType: TvType? = null private var currentEpisodes: List? = null + var downloadButton : EasyDownloadButton? = null override fun onCreateView( inflater: LayoutInflater, @@ -167,8 +168,15 @@ class ResultFragment : Fragment() { return inflater.inflate(R.layout.fragment_result, container, false) } + override fun onDestroyView() { + (result_episodes?.adapter as EpisodeAdapter?)?.killAdapter() + super.onDestroyView() + } + override fun onDestroy() { //requireActivity().viewModelStore.clear() // REMEMBER THE CLEAR + downloadButton?.dispose() + super.onDestroy() activity?.let { it.window?.navigationBarColor = @@ -873,8 +881,9 @@ class ResultFragment : Fragment() { val localId = d.getId() val file = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(requireContext(), localId) - - setUpMaterialButton( + downloadButton?.dispose() + downloadButton = EasyDownloadButton() + downloadButton?.setUpMaterialButton( file?.fileLength, file?.totalBytes, result_movie_progress_downloaded, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 891cd46c..da70421c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -131,8 +131,10 @@ class SearchFragment : Fragment() { when (it) { is Resource.Success -> { it.value.let { data -> - (cardSpace?.adapter as SearchAdapter?)?.cardList = data - cardSpace?.adapter?.notifyDataSetChanged() + if(data != null) { + (cardSpace?.adapter as SearchAdapter?)?.cardList = ArrayList( data.filterNotNull()) + cardSpace?.adapter?.notifyDataSetChanged() + } } searchExitIcon.alpha = 1f search_loading_bar.alpha = 0f diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/IDisposable.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/IDisposable.kt new file mode 100644 index 00000000..5d9580de --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/IDisposable.kt @@ -0,0 +1,12 @@ +package com.lagradost.cloudstream3.utils + +interface IDisposable { + fun dispose() +} + +object IDisposableHelper { + fun using(disposeObject: T, work: (T) -> Unit) { + work.invoke(disposeObject) + disposeObject.dispose() + } +} \ No newline at end of file