Skip to content
Open
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
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".DownloadsActivity"
android:exported="false"
android:label="Downloads"
android:parentActivityName=".MainActivity" />
</application>

</manifest>
72 changes: 72 additions & 0 deletions app/src/main/java/com/tpstreams/player/DownloadAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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

class DownloadAdapter : ListAdapter<DownloadItem, DownloadAdapter.DownloadViewHolder>(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)

Comment on lines +25 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace brittle findViewById with View Binding

Manual view look-ups are verbose, error-prone, and prevent the compiler from catching missing IDs.
Switching to View Binding (or Kotlin synthetic properties if you’re still on KTX) will:

• Remove the repeated casts
• Guarantee non-null references at compile time
• Reduce boilerplate in onCreateViewHolder / ViewHolder

Example refactor (View Binding):

-class DownloadViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
-    private val titleText: TextView = itemView.findViewById(R.id.title_text)
-    ...
+class DownloadViewHolder(
+    private val binding: ItemDownloadBinding
+) : RecyclerView.ViewHolder(binding.root) {
+    fun bind(downloadItem: DownloadItem) {
+        binding.titleText.text   = downloadItem.title
+        ...

And in onCreateViewHolder:

- val view = LayoutInflater.from(parent.context).inflate(R.layout.item_download, parent, false)
- return DownloadViewHolder(view)
+ val binding = ItemDownloadBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return DownloadViewHolder(binding)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/src/main/java/com/tpstreams/player/DownloadAdapter.kt around lines 25 to
31, replace the manual findViewById calls in DownloadViewHolder with View
Binding to avoid verbose and error-prone code. Enable View Binding in the
module, create a binding property for the item layout, and use it to directly
access views without casting. Update onCreateViewHolder to inflate the binding
instead of the layout and pass the binding.root to the ViewHolder constructor,
then access views through the binding instance inside the ViewHolder.

fun bind(downloadItem: DownloadItem) {
titleText.text = downloadItem.title
assetIdText.text = "Asset ID: ${downloadItem.assetId}"

// Set progress
val progress = downloadItem.progressPercentage.toInt()
progressBar.progress = progress

// Show only percentage
progressText.text = "$progress%"
Comment on lines +37 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Clamp / guard progress to avoid illegal values

progressPercentage can legally be C.PERCENTAGE_UNSET (‐1) or occasionally exceed 100 on bad manifests.
Directly casting to Int and assigning will:

• Display a “-1%” label
• Throw IllegalArgumentException on some OEM progress bars that validate range

-val progress = downloadItem.progressPercentage.toInt()
-progressBar.progress = progress
-progressText.text = "$progress%"
+val raw = downloadItem.progressPercentage
+val clamped = raw.coerceIn(0f, 100f).toInt()
+progressBar.isIndeterminate = raw < 0
+progressBar.progress = clamped
+progressText.text = if (raw < 0) "—" else "$clamped%"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val progress = downloadItem.progressPercentage.toInt()
progressBar.progress = progress
// Show only percentage
progressText.text = "$progress%"
val raw = downloadItem.progressPercentage
val clamped = raw.coerceIn(0f, 100f).toInt()
progressBar.isIndeterminate = raw < 0
progressBar.progress = clamped
// Show only percentage
progressText.text = if (raw < 0) "" else "$clamped%"
🤖 Prompt for AI Agents
In app/src/main/java/com/tpstreams/player/DownloadAdapter.kt around lines 37 to
41, the progress value derived from downloadItem.progressPercentage can be -1 or
exceed 100, which causes invalid progress bar states and exceptions. To fix
this, clamp the progress value to a valid range between 0 and 100 before
assigning it to progressBar.progress and displaying it in progressText. This
prevents illegal values and ensures safe UI updates.


// Set state text
stateText.text = getStateString(downloadItem.state)
}



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<DownloadItem>() {
override fun areItemsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
return oldItem.assetId == newItem.assetId
}

override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
return oldItem == newItem
}
}
55 changes: 55 additions & 0 deletions app/src/main/java/com/tpstreams/player/DownloadViewModel.kt
Original file line number Diff line number Diff line change
@@ -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<List<DownloadItem>>()
val downloads: LiveData<List<DownloadItem>> = _downloads

private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
Comment on lines +17 to +18
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Initialize _isLoading with a default value to avoid null emissions

MutableLiveData<Boolean>() starts out as null, so observers must defensively handle the first null emission.
Given _isLoading is effectively a boolean flag, initialise it to false so the UI never has to deal with an unexpected null.

-private val _isLoading = MutableLiveData<Boolean>()
+private val _isLoading = MutableLiveData(false)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean> = _isLoading
🤖 Prompt for AI Agents
In app/src/main/java/com/tpstreams/player/DownloadViewModel.kt around lines 17
to 18, the MutableLiveData _isLoading is not initialized with a default value,
causing it to emit null initially. Fix this by initializing _isLoading with
false to ensure observers always receive a non-null Boolean value and the UI
does not have to handle null cases.


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
Comment on lines +32 to +36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Off-load loadDownloads() to a background thread

downloadTracker.getAllDownloadItems() can touch disk / network and is executed on the main thread, risking UI jank.
Run the fetch on Dispatchers.IO and post the result back:

-fun loadDownloads() {
-    _isLoading.value = true
-    val downloadItems = downloadTracker.getAllDownloadItems()
-    _downloads.value = downloadItems
-    _isLoading.value = false
-}
+fun loadDownloads() = viewModelScope.launch {
+    _isLoading.postValue(true)
+    val downloadItems = withContext(Dispatchers.IO) {
+        downloadTracker.getAllDownloadItems()
+    }
+    _downloads.postValue(downloadItems)
+    _isLoading.postValue(false)
+}

(remember to add import androidx.lifecycle.viewModelScope and coroutine deps if not already present).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun loadDownloads() {
_isLoading.value = true
val downloadItems = downloadTracker.getAllDownloadItems()
_downloads.value = downloadItems
_isLoading.value = false
// remember to import:
// import androidx.lifecycle.viewModelScope
// import kotlinx.coroutines.Dispatchers
// import kotlinx.coroutines.withContext
fun loadDownloads() = viewModelScope.launch {
_isLoading.postValue(true)
val downloadItems = withContext(Dispatchers.IO) {
downloadTracker.getAllDownloadItems()
}
_downloads.postValue(downloadItems)
_isLoading.postValue(false)
}
🤖 Prompt for AI Agents
In app/src/main/java/com/tpstreams/player/DownloadViewModel.kt around lines 32
to 36, the loadDownloads() function performs disk or network operations on the
main thread, which can cause UI jank. To fix this, wrap the call to
downloadTracker.getAllDownloadItems() inside a coroutine launched on
viewModelScope with Dispatchers.IO, then update _downloads and _isLoading on the
main thread after the fetch completes. Also, ensure you import
androidx.lifecycle.viewModelScope and have coroutine dependencies configured.

}

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)
}
}
130 changes: 130 additions & 0 deletions app/src/main/java/com/tpstreams/player/DownloadsActivity.kt
Original file line number Diff line number Diff line change
@@ -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()
}
})

Comment on lines +38 to +43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unregister the AdapterDataObserver to avoid leaking the Activity

RecyclerView.AdapterDataObserver holds a strong reference to the outer Activity via the anonymous class.
Add onDestroy { adapter.unregisterAdapterDataObserver(observer) } (where observer is stored in a field) or drop the observer since checkEmptyState() is already invoked from the downloads LiveData observer.

🤖 Prompt for AI Agents
In app/src/main/java/com/tpstreams/player/DownloadsActivity.kt around lines 38
to 43, the AdapterDataObserver is registered anonymously without being
unregistered, causing a potential memory leak by holding a strong reference to
the Activity. To fix this, store the observer instance in a field, then override
onDestroy() to unregister this observer from the adapter using
adapter.unregisterAdapterDataObserver(observer). Alternatively, if
checkEmptyState() is already called from the downloads LiveData observer, you
can remove the AdapterDataObserver registration entirely.

// 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()
}
Comment on lines +75 to +111
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use stable menu item IDs instead of comparing titles

Comparing menuItem.title against translated strings can break in localized builds. Create items with fixed IDs and switch on item.itemId:

-        when (downloadItem.state) {
+        when (downloadItem.state) {
             Download.STATE_COMPLETED -> {
-                popupMenu.menu.add(getString(R.string.delete_download_title))
+                popupMenu.menu.add(0, R.id.menu_delete, 0, R.string.delete_download_title)
             }
             ...
         }
 
-        popupMenu.setOnMenuItemClickListener { menuItem ->
-            when (menuItem.title) {
-                getString(R.string.delete_download_title), getString(R.string.cancel_download) -> {
+        popupMenu.setOnMenuItemClickListener { menuItem ->
+            when (menuItem.itemId) {
+                R.id.menu_delete, R.id.menu_cancel -> {
                     viewModel.removeDownload(downloadItem.assetId)
                 }
-                getString(R.string.pause_download) -> {
+                R.id.menu_pause -> {
                     viewModel.pauseDownload(downloadItem.assetId)
                 }
-                getString(R.string.resume_download) -> {
+                R.id.menu_resume -> {
                     viewModel.resumeDownload(downloadItem.assetId)
                 }
             }
             true
         }

Remember to declare these IDs in res/values/ids.xml or use existing ones.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 showPopupMenu(view: View, downloadItem: DownloadItem) {
val popupMenu = PopupMenu(this, view)
when (downloadItem.state) {
Download.STATE_COMPLETED -> {
popupMenu.menu.add(0, R.id.menu_delete, 0, R.string.delete_download_title)
}
Download.STATE_DOWNLOADING -> {
popupMenu.menu.add(0, R.id.menu_pause, 0, R.string.pause_download)
popupMenu.menu.add(0, R.id.menu_cancel, 0, R.string.cancel_download)
}
Download.STATE_STOPPED -> {
popupMenu.menu.add(0, R.id.menu_resume, 0, R.string.resume_download)
popupMenu.menu.add(0, R.id.menu_cancel, 0, R.string.cancel_download)
}
else -> {
popupMenu.menu.add(0, R.id.menu_cancel, 0, R.string.cancel_download)
}
}
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.menu_delete, R.id.menu_cancel -> {
viewModel.removeDownload(downloadItem.assetId)
}
R.id.menu_pause -> {
viewModel.pauseDownload(downloadItem.assetId)
}
R.id.menu_resume -> {
viewModel.resumeDownload(downloadItem.assetId)
}
}
true
}
popupMenu.show()
}
🤖 Prompt for AI Agents
In app/src/main/java/com/tpstreams/player/DownloadsActivity.kt between lines 75
and 111, the popup menu item click handling compares menuItem.title against
localized strings, which can break in localized builds. To fix this, assign
fixed, stable IDs to each menu item when adding them to the popup menu, then in
the setOnMenuItemClickListener switch on menuItem.itemId instead of title.
Define these IDs in res/values/ids.xml or use existing resource IDs to ensure
consistency across locales.


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)
}
}
9 changes: 8 additions & 1 deletion app/src/main/java/com/tpstreams/player/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.tpstreams.player

import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
Expand All @@ -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)
}
}
}
4 changes: 2 additions & 2 deletions app/src/main/java/com/tpstreams/player/PlayerUIViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment on lines +15 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid committing credentials / tokens to VCS

assetId and especially accessToken are hard-coded. Access tokens are usually short-lived secrets and should reside in secure storage (remote config, keystore, or injected via CI build configs).

At minimum, move them to BuildConfig fields that are excluded from version control.

-assetId = "BEArYFdaFbt",
-accessToken = "e6a1b485-daad-42eb-8cf2-6b6e51631092",
+assetId = BuildConfig.TP_ASSET_ID,
+accessToken = BuildConfig.TP_ACCESS_TOKEN,

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/src/main/java/com/tpstreams/player/PlayerUIViewModel.kt around lines 15
to 16, the assetId and accessToken are hard-coded, exposing sensitive
credentials in version control. To fix this, remove these hard-coded values and
instead retrieve them from BuildConfig fields or a secure storage mechanism.
Configure these fields in your build system or CI pipeline so that the secrets
are injected at build time and not stored in the source code repository.

shouldAutoPlay = false
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {}
}
Loading