package com.michatec.radio.search import android.content.Context import android.media.AudioAttributes import android.media.AudioFocusRequest import android.media.AudioManager import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.annotation.OptIn import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.media3.common.* import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.audio.AudioSink import androidx.media3.exoplayer.audio.DefaultAudioSink import androidx.recyclerview.widget.RecyclerView import com.google.android.material.textview.MaterialTextView import com.michatec.radio.R import com.michatec.radio.core.Station import com.michatec.radio.helpers.NativeAudioProcessor /* * SearchResultAdapter class */ class SearchResultAdapter( private val listener: SearchResultAdapterListener, var searchResults: List) : RecyclerView.Adapter() { /* Main class variables */ private var selectedPosition: Int = RecyclerView.NO_POSITION private var exoPlayer: ExoPlayer? = null private var paused: Boolean = false private var isItemSelected: Boolean = false private var nativeAudioProcessor = NativeAudioProcessor() /* Listener Interface */ interface SearchResultAdapterListener { fun onSearchResultTapped(result: Station) fun activateAddButton() fun deactivateAddButton() } init { setHasStableIds(true) } /* Overrides onCreateViewHolder from RecyclerView.Adapter */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val v = LayoutInflater.from(parent.context) .inflate(R.layout.element_search_result, parent, false) return SearchResultViewHolder(v) } /* Overrides getItemCount from RecyclerView.Adapter */ override fun getItemCount(): Int { return searchResults.size } /* Overrides getItemCount from RecyclerView.Adapter */ override fun getItemId(position: Int): Long = position.toLong() /* Overrides onBindViewHolder from RecyclerView.Adapter */ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { // get reference to ViewHolder val searchResultViewHolder: SearchResultViewHolder = holder as SearchResultViewHolder val searchResult: Station = searchResults[position] // update text searchResultViewHolder.nameView.text = searchResult.name searchResultViewHolder.streamView.text = searchResult.getStreamUri() if (searchResult.codec.isNotEmpty()) { if (searchResult.bitrate == 0) { // show only the codec when the bitrate is at "0" from radio-browser.info API searchResultViewHolder.bitrateView.text = searchResult.codec } else { // show the bitrate and codec if the result is available in the radio-browser.info API searchResultViewHolder.bitrateView.text = buildString { append(searchResult.codec) append(" | ") append(searchResult.bitrate) append("kbps")} } } else { // do not show for M3U and PLS playlists as they do not include codec or bitrate searchResultViewHolder.bitrateView.visibility = View.GONE } // mark selected if necessary val isSelected = selectedPosition == holder.bindingAdapterPosition searchResultViewHolder.searchResultLayout.isSelected = isSelected // toggle text scrolling (marquee) if necessary searchResultViewHolder.nameView.isSelected = isSelected searchResultViewHolder.streamView.isSelected = isSelected // reduce the shadow left and right because of scrolling (Marquee) searchResultViewHolder.nameView.setFadingEdgeLength(10) searchResultViewHolder.streamView.setFadingEdgeLength(10) // attach touch listener searchResultViewHolder.searchResultLayout.setOnClickListener { // move marked position val previousSelectedPosition = selectedPosition selectedPosition = holder.bindingAdapterPosition notifyItemChanged(previousSelectedPosition) notifyItemChanged(selectedPosition) // check if the selected position is the same as before val samePositionSelected = previousSelectedPosition == selectedPosition if (samePositionSelected) { // if the same position is selected again, reset the selection resetSelection(false) } else { // get the selected station from searchResults val selectedStation = searchResults[holder.bindingAdapterPosition] // perform pre-playback here performPrePlayback(searchResultViewHolder.searchResultLayout.context, selectedStation.getStreamUri()) // hand over station listener.onSearchResultTapped(searchResult) } // update isItemSelected based on the selection isItemSelected = !samePositionSelected // enable/disable the Add button based on isItemSelected if (isItemSelected) { listener.activateAddButton() } else { listener.deactivateAddButton() } } } @OptIn(UnstableApi::class) private fun performPrePlayback(context: Context, streamUri: String) { if (streamUri.contains(".m3u8")) { // release previous player if it exists stopPrePlayback() // show toast when no playback is possible Toast.makeText(context, R.string.toastmessage_preview_playback_failed, Toast.LENGTH_SHORT).show() } else { stopRadioPlayback(context) // release previous player if it exists stopPrePlayback() // set up audio attributes for the preview player val audioAttributes = androidx.media3.common.AudioAttributes.Builder() .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) .setUsage(C.USAGE_MEDIA) .build() // Create a RenderersFactory that injects the NativeAudioProcessor val renderersFactory = object : DefaultRenderersFactory(context) { override fun buildAudioSink( context: Context, enableFloatOutput: Boolean, enableAudioTrackPlaybackParams: Boolean ): AudioSink { return DefaultAudioSink.Builder(context) .setAudioProcessors(arrayOf(nativeAudioProcessor)) .build() } } // create a new instance of ExoPlayer with focus handling exoPlayer = ExoPlayer.Builder(context, renderersFactory) .setAudioAttributes(audioAttributes, true) .setHandleAudioBecomingNoisy(true) .build() // create a MediaItem with the streamUri val mediaItem = MediaItem.fromUri(streamUri) // set the MediaItem to the ExoPlayer exoPlayer?.setMediaItem(mediaItem) // prepare and start the ExoPlayer exoPlayer?.prepare() exoPlayer?.play() // show toast when playback is possible Toast.makeText(context, R.string.toastmessage_preview_playback_started, Toast.LENGTH_SHORT).show() // listen for app pause events val lifecycle = (context as AppCompatActivity).lifecycle val lifecycleObserver = object : DefaultLifecycleObserver { override fun onPause(owner: LifecycleOwner) { if (!paused) { paused = true stopPrePlayback() } } } lifecycle.addObserver(lifecycleObserver) } } fun stopPrePlayback() { // stop the ExoPlayer and release resources exoPlayer?.stop() exoPlayer?.release() exoPlayer = null } private fun stopRadioPlayback(context: Context) { // stop radio playback when one is active val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager val audioAttributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build() val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(audioAttributes) .build() audioManager.requestAudioFocus(focusRequest) } /* Resets the selected position */ fun resetSelection(clearAdapter: Boolean) { val currentlySelected: Int = selectedPosition selectedPosition = RecyclerView.NO_POSITION if (clearAdapter) { val previousItemCount = itemCount searchResults = emptyList() notifyItemRangeRemoved(0, previousItemCount) } else { notifyItemChanged(currentlySelected) stopPrePlayback() } } /* * Inner class: ViewHolder for a radio station search result */ private class SearchResultViewHolder(var searchResultLayout: View) : RecyclerView.ViewHolder(searchResultLayout) { val nameView: MaterialTextView = searchResultLayout.findViewById(R.id.station_name) val streamView: MaterialTextView = searchResultLayout.findViewById(R.id.station_url) val bitrateView: MaterialTextView = searchResultLayout.findViewById(R.id.station_bitrate) } }