From e443380df881bc401125e95b02be53da5502f169 Mon Sep 17 00:00:00 2001 From: Syed Ibrahim Date: Tue, 17 Jun 2025 10:03:34 +0530 Subject: [PATCH 1/2] Add download list screen in app --- app/src/main/AndroidManifest.xml | 5 + .../com/tpstreams/player/DownloadAdapter.kt | 83 +++++++++++ .../com/tpstreams/player/DownloadViewModel.kt | 55 ++++++++ .../com/tpstreams/player/DownloadsActivity.kt | 130 ++++++++++++++++++ .../java/com/tpstreams/player/MainActivity.kt | 9 +- .../com/tpstreams/player/PlayerUIViewModel.kt | 4 +- .../player/RecyclerItemClickListener.kt | 49 +++++++ .../main/res/layout/activity_downloads.xml | 53 +++++++ app/src/main/res/layout/activity_main.xml | 10 ++ app/src/main/res/layout/item_download.xml | 72 ++++++++++ app/src/main/res/values/strings.xml | 19 +++ 11 files changed, 486 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/tpstreams/player/DownloadAdapter.kt create mode 100644 app/src/main/java/com/tpstreams/player/DownloadViewModel.kt create mode 100644 app/src/main/java/com/tpstreams/player/DownloadsActivity.kt create mode 100644 app/src/main/java/com/tpstreams/player/RecyclerItemClickListener.kt create mode 100644 app/src/main/res/layout/activity_downloads.xml create mode 100644 app/src/main/res/layout/item_download.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0e7c882..8a883db 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,6 +24,11 @@ + \ No newline at end of file diff --git a/app/src/main/java/com/tpstreams/player/DownloadAdapter.kt b/app/src/main/java/com/tpstreams/player/DownloadAdapter.kt new file mode 100644 index 0000000..5039df9 --- /dev/null +++ b/app/src/main/java/com/tpstreams/player/DownloadAdapter.kt @@ -0,0 +1,83 @@ +package com.tpstreams.player + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ProgressBar +import android.widget.TextView +import androidx.media3.exoplayer.offline.Download +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.tpstreams.player.download.DownloadItem +import java.text.DecimalFormat + +class DownloadAdapter : ListAdapter(DownloadDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_download, parent, false) + return DownloadViewHolder(view) + } + + override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + class DownloadViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val titleText: TextView = itemView.findViewById(R.id.title_text) + private val assetIdText: TextView = itemView.findViewById(R.id.asset_id_text) + private val progressBar: ProgressBar = itemView.findViewById(R.id.download_progress) + private val progressText: TextView = itemView.findViewById(R.id.progress_text) + private val stateText: TextView = itemView.findViewById(R.id.state_text) + + fun bind(downloadItem: DownloadItem) { + titleText.text = downloadItem.title + assetIdText.text = "Asset ID: ${downloadItem.assetId}" + + // Set progress + val progress = downloadItem.progressPercentage.toInt() + progressBar.progress = progress + + // Format file sizes + val downloadedSize = formatFileSize(downloadItem.downloadedBytes) + val totalSize = formatFileSize(downloadItem.totalBytes) + progressText.text = "$progress% • $downloadedSize / $totalSize" + + // Set state text + stateText.text = getStateString(downloadItem.state) + } + + private fun formatFileSize(bytes: Long): String { + val df = DecimalFormat("0.00") + return when { + bytes < 1024 -> "$bytes B" + bytes < 1024 * 1024 -> "${df.format(bytes / 1024.0)} KB" + bytes < 1024 * 1024 * 1024 -> "${df.format(bytes / (1024.0 * 1024.0))} MB" + else -> "${df.format(bytes / (1024.0 * 1024.0 * 1024.0))} GB" + } + } + + private fun getStateString(state: Int): String { + return when (state) { + Download.STATE_COMPLETED -> "COMPLETED" + Download.STATE_DOWNLOADING -> "DOWNLOADING" + Download.STATE_FAILED -> "FAILED" + Download.STATE_QUEUED -> "QUEUED" + Download.STATE_REMOVING -> "REMOVING" + Download.STATE_RESTARTING -> "RESTARTING" + Download.STATE_STOPPED -> "PAUSED" + else -> "UNKNOWN" + } + } + } +} + +class DownloadDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean { + return oldItem.assetId == newItem.assetId + } + + override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean { + return oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tpstreams/player/DownloadViewModel.kt b/app/src/main/java/com/tpstreams/player/DownloadViewModel.kt new file mode 100644 index 0000000..c005801 --- /dev/null +++ b/app/src/main/java/com/tpstreams/player/DownloadViewModel.kt @@ -0,0 +1,55 @@ +package com.tpstreams.player + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.media3.common.util.UnstableApi +import com.tpstreams.player.download.DownloadItem +import com.tpstreams.player.download.DownloadTracker + +@UnstableApi +class DownloadViewModel(application: Application) : AndroidViewModel(application) { + + private val _downloads = MutableLiveData>() + val downloads: LiveData> = _downloads + + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + private val downloadTracker = DownloadTracker.getInstance(application) + private val downloadListener = object : DownloadTracker.Listener { + override fun onDownloadsChanged() { + loadDownloads() + } + } + + init { + downloadTracker.addListener(downloadListener) + loadDownloads() + } + + fun loadDownloads() { + _isLoading.value = true + val downloadItems = downloadTracker.getAllDownloadItems() + _downloads.value = downloadItems + _isLoading.value = false + } + + fun pauseDownload(assetId: String) { + downloadTracker.pauseDownload(assetId) + } + + fun resumeDownload(assetId: String) { + downloadTracker.resumeDownload(assetId) + } + + fun removeDownload(assetId: String) { + downloadTracker.removeDownload(assetId) + } + + override fun onCleared() { + super.onCleared() + downloadTracker.removeListener(downloadListener) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tpstreams/player/DownloadsActivity.kt b/app/src/main/java/com/tpstreams/player/DownloadsActivity.kt new file mode 100644 index 0000000..804fe19 --- /dev/null +++ b/app/src/main/java/com/tpstreams/player/DownloadsActivity.kt @@ -0,0 +1,130 @@ +package com.tpstreams.player + +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.widget.PopupMenu +import androidx.activity.viewModels +import androidx.annotation.OptIn +import androidx.appcompat.app.AppCompatActivity +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.offline.Download +import androidx.recyclerview.widget.LinearLayoutManager +import com.tpstreams.player.databinding.ActivityDownloadsBinding +import com.tpstreams.player.download.DownloadItem + +@OptIn(UnstableApi::class) +class DownloadsActivity : AppCompatActivity() { + + private lateinit var binding: ActivityDownloadsBinding + private val viewModel: DownloadViewModel by viewModels() + private lateinit var adapter: DownloadAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityDownloadsBinding.inflate(layoutInflater) + setContentView(binding.root) + + // Setup toolbar + setSupportActionBar(binding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + // Setup RecyclerView + adapter = DownloadAdapter() + binding.downloadsRecyclerView.adapter = adapter + binding.downloadsRecyclerView.layoutManager = LinearLayoutManager(this) + + // Setup item click listener + adapter.registerAdapterDataObserver(object : androidx.recyclerview.widget.RecyclerView.AdapterDataObserver() { + override fun onChanged() { + checkEmptyState() + } + }) + + // Observe downloads + viewModel.downloads.observe(this) { downloads -> + adapter.submitList(downloads) + checkEmptyState() + } + + // Observe loading state + viewModel.isLoading.observe(this) { isLoading -> + binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + } + + // Setup item click listener with popup menu + setupItemClickListener() + } + + private fun setupItemClickListener() { + binding.downloadsRecyclerView.addOnItemTouchListener( + RecyclerItemClickListener(this, binding.downloadsRecyclerView, + object : RecyclerItemClickListener.OnItemClickListener { + override fun onItemClick(view: View, position: Int) { + val downloadItem = adapter.currentList[position] + showPopupMenu(view, downloadItem) + } + + override fun onLongItemClick(view: View, position: Int) { + // Not needed for now + } + }) + ) + } + + private fun showPopupMenu(view: View, downloadItem: DownloadItem) { + val popupMenu = PopupMenu(this, view) + + when (downloadItem.state) { + Download.STATE_COMPLETED -> { + popupMenu.menu.add(getString(R.string.delete_download_title)) + } + Download.STATE_DOWNLOADING -> { + popupMenu.menu.add(getString(R.string.pause_download)) + popupMenu.menu.add(getString(R.string.cancel_download)) + } + Download.STATE_STOPPED -> { + popupMenu.menu.add(getString(R.string.resume_download)) + popupMenu.menu.add(getString(R.string.cancel_download)) + } + else -> { + popupMenu.menu.add(getString(R.string.cancel_download)) + } + } + + popupMenu.setOnMenuItemClickListener { menuItem -> + when (menuItem.title) { + getString(R.string.delete_download_title), getString(R.string.cancel_download) -> { + viewModel.removeDownload(downloadItem.assetId) + } + getString(R.string.pause_download) -> { + viewModel.pauseDownload(downloadItem.assetId) + } + getString(R.string.resume_download) -> { + viewModel.resumeDownload(downloadItem.assetId) + } + } + true + } + + popupMenu.show() + } + + private fun checkEmptyState() { + if (adapter.itemCount == 0) { + binding.emptyView.visibility = View.VISIBLE + binding.downloadsRecyclerView.visibility = View.GONE + } else { + binding.emptyView.visibility = View.GONE + binding.downloadsRecyclerView.visibility = View.VISIBLE + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + finish() + return true + } + return super.onOptionsItemSelected(item) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tpstreams/player/MainActivity.kt b/app/src/main/java/com/tpstreams/player/MainActivity.kt index 4fc1ed4..bccba26 100644 --- a/app/src/main/java/com/tpstreams/player/MainActivity.kt +++ b/app/src/main/java/com/tpstreams/player/MainActivity.kt @@ -1,5 +1,6 @@ package com.tpstreams.player +import android.content.Intent import android.os.Bundle import android.util.Log import androidx.activity.viewModels @@ -22,8 +23,14 @@ class MainActivity : AppCompatActivity() { setContentView(binding.root) // Initialize SDK once - TPStreamsPlayer.init("6332n7") + TPStreamsPlayer.init("9q94nm") binding.playerView.player = viewModel.player + + // Set up downloads button + binding.downloadsButton.setOnClickListener { + val intent = Intent(this, DownloadsActivity::class.java) + startActivity(intent) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tpstreams/player/PlayerUIViewModel.kt b/app/src/main/java/com/tpstreams/player/PlayerUIViewModel.kt index b9b3f6e..f68706b 100644 --- a/app/src/main/java/com/tpstreams/player/PlayerUIViewModel.kt +++ b/app/src/main/java/com/tpstreams/player/PlayerUIViewModel.kt @@ -12,8 +12,8 @@ class PlayerUIViewModel(application: Application) : AndroidViewModel(application val player: TPStreamsPlayer by lazy { TPStreamsPlayer.create( context = application.applicationContext, - assetId = "8rEx9apZHFF", - accessToken = "19aa0055-d965-4654-8fce-b804e70a46b0", + assetId = "BEArYFdaFbt", + accessToken = "e6a1b485-daad-42eb-8cf2-6b6e51631092", shouldAutoPlay = false ) } diff --git a/app/src/main/java/com/tpstreams/player/RecyclerItemClickListener.kt b/app/src/main/java/com/tpstreams/player/RecyclerItemClickListener.kt new file mode 100644 index 0000000..7e123e3 --- /dev/null +++ b/app/src/main/java/com/tpstreams/player/RecyclerItemClickListener.kt @@ -0,0 +1,49 @@ +package com.tpstreams.player + +import android.content.Context +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +class RecyclerItemClickListener( + context: Context, + recyclerView: RecyclerView, + private val listener: OnItemClickListener? +) : RecyclerView.OnItemTouchListener { + + interface OnItemClickListener { + fun onItemClick(view: View, position: Int) + fun onLongItemClick(view: View, position: Int) + } + + private val gestureDetector: GestureDetector + + init { + gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { + override fun onSingleTapUp(e: MotionEvent): Boolean { + return true + } + + override fun onLongPress(e: MotionEvent) { + val childView = recyclerView.findChildViewUnder(e.x, e.y) + if (childView != null && listener != null) { + listener.onLongItemClick(childView, recyclerView.getChildAdapterPosition(childView)) + } + } + }) + } + + override fun onInterceptTouchEvent(view: RecyclerView, e: MotionEvent): Boolean { + val childView = view.findChildViewUnder(e.x, e.y) + if (childView != null && listener != null && gestureDetector.onTouchEvent(e)) { + listener.onItemClick(childView, view.getChildAdapterPosition(childView)) + return true + } + return false + } + + override fun onTouchEvent(view: RecyclerView, motionEvent: MotionEvent) {} + + override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {} +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_downloads.xml b/app/src/main/res/layout/activity_downloads.xml new file mode 100644 index 0000000..52b2a41 --- /dev/null +++ b/app/src/main/res/layout/activity_downloads.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ecd6e3f..8ca868a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -16,4 +16,14 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> +