mirror of
https://github.com/Michatec/Radio.git
synced 2026-04-01 16:06:27 +02:00
Initial commit
This commit is contained in:
111
app/src/main/java/com/michatec/radio/search/DirectInputCheck.kt
Normal file
111
app/src/main/java/com/michatec/radio/search/DirectInputCheck.kt
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* DirectInputCheck.kt
|
||||
* Implements the DirectInputCheck class
|
||||
* A DirectInputCheck checks if a station url is valid and returns station via a listener
|
||||
*
|
||||
* This file is part of
|
||||
* TRANSISTOR - Radio App for Android
|
||||
*
|
||||
* Copyright (c) 2015-23 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
|
||||
package com.michatec.radio.search
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.URLUtil
|
||||
import android.widget.Toast
|
||||
import com.michatec.radio.R
|
||||
import com.michatec.radio.core.Station
|
||||
import com.michatec.radio.helpers.CollectionHelper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.util.GregorianCalendar
|
||||
|
||||
|
||||
data class IcecastMetadata(
|
||||
val title: String?
|
||||
)
|
||||
/*
|
||||
* DirectInputCheck class
|
||||
*/
|
||||
class DirectInputCheck(private var directInputCheckListener: DirectInputCheckListener) {
|
||||
|
||||
/* Interface used to send back station list for checked */
|
||||
interface DirectInputCheckListener {
|
||||
fun onDirectInputCheck(stationList: MutableList<Station>) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private var lastCheckedAddress: String = String()
|
||||
|
||||
|
||||
/* Searches station(s) on radio-browser.info */
|
||||
fun checkStationAddress(context: Context, query: String) {
|
||||
// check if valid URL
|
||||
if (URLUtil.isValidUrl(query)) {
|
||||
val stationList: MutableList<Station> = mutableListOf()
|
||||
CoroutineScope(IO).launch {
|
||||
stationList.addAll(CollectionHelper.createStationsFromUrl(query, lastCheckedAddress))
|
||||
lastCheckedAddress = query
|
||||
withContext(Main) {
|
||||
if (stationList.isNotEmpty()) {
|
||||
// hand over station is to listener
|
||||
directInputCheckListener.onDirectInputCheck(stationList)
|
||||
} else {
|
||||
// invalid address
|
||||
Toast.makeText(context, R.string.toastmessage_station_not_valid, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun extractIcecastMetadata(streamUri: String): IcecastMetadata {
|
||||
return withContext(IO) {
|
||||
// make an HTTP request at the stream URL to get Icecast metadata.
|
||||
val client = OkHttpClient()
|
||||
val request = Request.Builder()
|
||||
.url(streamUri)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val icecastHeaders = response.headers
|
||||
|
||||
// analyze the Icecast metadata and extract information like title, description, bitrate, etc.
|
||||
val title = icecastHeaders["icy-name"]
|
||||
|
||||
IcecastMetadata(title?.takeIf { it.isNotEmpty() } ?: streamUri)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun updateStationWithIcecastMetadata(station: Station, icecastMetadata: IcecastMetadata) {
|
||||
withContext(Dispatchers.Default) {
|
||||
station.name = icecastMetadata.title.toString()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun processIcecastStream(streamUri: String, stationList: MutableList<Station>) {
|
||||
val icecastMetadata = extractIcecastMetadata(streamUri)
|
||||
val station = Station(name = icecastMetadata.title.toString(), streamUris = mutableListOf(streamUri), modificationDate = GregorianCalendar.getInstance().time)
|
||||
updateStationWithIcecastMetadata(station, icecastMetadata)
|
||||
// create station and add to collection
|
||||
if (lastCheckedAddress != streamUri) {
|
||||
stationList.add(station)
|
||||
}
|
||||
lastCheckedAddress = streamUri
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* RadioBrowserResult.kt
|
||||
* Implements the RadioBrowserResult class
|
||||
* A RadioBrowserResult is the search result of a request to api.radio-browser.info
|
||||
*
|
||||
* This file is part of
|
||||
* TRANSISTOR - Radio App for Android
|
||||
*
|
||||
* Copyright (c) 2015-22 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
|
||||
package com.michatec.radio.search
|
||||
|
||||
import com.google.gson.annotations.Expose
|
||||
import com.michatec.radio.Keys
|
||||
import com.michatec.radio.core.Station
|
||||
import java.util.*
|
||||
|
||||
|
||||
/*
|
||||
* RadioBrowserResult class
|
||||
*/
|
||||
data class RadioBrowserResult(
|
||||
@Expose val changeuuid: String,
|
||||
@Expose val stationuuid: String,
|
||||
@Expose val name: String,
|
||||
@Expose val url: String,
|
||||
@Expose val url_resolved: String,
|
||||
@Expose val homepage: String,
|
||||
@Expose val favicon: String,
|
||||
@Expose val bitrate: Int,
|
||||
@Expose val codec: String
|
||||
) {
|
||||
|
||||
/* Converts RadioBrowserResult to Station */
|
||||
fun toStation(): Station = Station(
|
||||
starred = false,
|
||||
name = name,
|
||||
nameManuallySet = false,
|
||||
streamUris = mutableListOf(url_resolved),
|
||||
stream = 0,
|
||||
streamContent = Keys.MIME_TYPE_UNSUPPORTED,
|
||||
homepage = homepage,
|
||||
image = String(),
|
||||
smallImage = String(),
|
||||
imageColor = -1,
|
||||
imageManuallySet = false,
|
||||
remoteImageLocation = favicon,
|
||||
remoteStationLocation = url,
|
||||
modificationDate = GregorianCalendar.getInstance().time,
|
||||
isPlaying = false,
|
||||
radioBrowserStationUuid = stationuuid,
|
||||
radioBrowserChangeUuid = changeuuid,
|
||||
bitrate = bitrate,
|
||||
codec = codec
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
JSON Struct Station
|
||||
https://de1.api.radio-browser.info/
|
||||
|
||||
changeuuid UUID A globally unique identifier for the change of the station information
|
||||
stationuuid UUID A globally unique identifier for the station
|
||||
name string The name of the station
|
||||
url string, URL (HTTP/HTTPS) The stream URL provided by the user
|
||||
url_resolved string, URL (HTTP/HTTPS) An automatically "resolved" stream URL. Things resolved are playlists (M3U/PLS/ASX...), HTTP redirects (Code 301/302). This link is especially usefull if you use this API from a platform that is not able to do a resolve on its own (e.g. JavaScript in browser) or you just don't want to invest the time in decoding playlists yourself.
|
||||
homepage string, URL (HTTP/HTTPS) URL to the homepage of the stream, so you can direct the user to a page with more information about the stream.
|
||||
favicon string, URL (HTTP/HTTPS) URL to an icon or picture that represents the stream. (PNG, JPG)
|
||||
tags string, multivalue, split by comma Tags of the stream with more information about it
|
||||
country string DEPRECATED: use countrycode instead, full name of the country
|
||||
countrycode 2 letters, uppercase Official countrycodes as in ISO 3166-1 alpha-2
|
||||
state string Full name of the entity where the station is located inside the country
|
||||
language string, multivalue, split by comma Languages that are spoken in this stream.
|
||||
votes number, integer Number of votes for this station. This number is by server and only ever increases. It will never be reset to 0.
|
||||
lastchangetime datetime, YYYY-MM-DD HH:mm:ss Last time when the stream information was changed in the database
|
||||
codec string The codec of this stream recorded at the last check.
|
||||
bitrate number, integer, bps The bitrate of this stream recorded at the last check.
|
||||
hls 0 or 1 Mark if this stream is using HLS distribution or non-HLS.
|
||||
lastcheckok 0 or 1 The current online/offline state of this stream. This is a value calculated from multiple measure points in the internet. The test servers are located in different countries. It is a majority vote.
|
||||
lastchecktime datetime, YYYY-MM-DD HH:mm:ss The last time when any radio-browser server checked the online state of this stream
|
||||
lastcheckoktime datetime, YYYY-MM-DD HH:mm:ss The last time when the stream was checked for the online status with a positive result
|
||||
lastlocalchecktime datetime, YYYY-MM-DD HH:mm:ss The last time when this server checked the online state and the metadata of this stream
|
||||
clicktimestamp datetime, YYYY-MM-DD HH:mm:ss The time of the last click recorded for this stream
|
||||
clickcount number, integer Clicks within the last 24 hours
|
||||
clicktrend number, integer The difference of the clickcounts within the last 2 days. Posivite values mean an increase, negative a decrease of clicks.
|
||||
*/
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* RadioBrowserSearch.kt
|
||||
* Implements the RadioBrowserSearch class
|
||||
* A RadioBrowserSearch performs searches on the radio-browser.info database
|
||||
*
|
||||
* This file is part of
|
||||
* TRANSISTOR - Radio App for Android
|
||||
*
|
||||
* Copyright (c) 2015-22 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
|
||||
package com.michatec.radio.search
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.android.volley.*
|
||||
import com.android.volley.toolbox.JsonArrayRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
import com.google.gson.GsonBuilder
|
||||
import org.json.JSONArray
|
||||
import com.michatec.radio.BuildConfig
|
||||
import com.michatec.radio.Keys
|
||||
import com.michatec.radio.helpers.NetworkHelper
|
||||
import com.michatec.radio.helpers.PreferencesHelper
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
|
||||
|
||||
/*
|
||||
* RadioBrowserSearch class
|
||||
*/
|
||||
class RadioBrowserSearch(private var radioBrowserSearchListener: RadioBrowserSearchListener) {
|
||||
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = RadioBrowserSearch::class.java.simpleName
|
||||
|
||||
|
||||
/* Interface used to send back search results */
|
||||
interface RadioBrowserSearchListener {
|
||||
fun onRadioBrowserSearchResults(results: Array<RadioBrowserResult>) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Main class variables */
|
||||
private var radioBrowserApi: String
|
||||
private lateinit var requestQueue: RequestQueue
|
||||
|
||||
|
||||
/* Init constructor */
|
||||
init {
|
||||
// get address of radio-browser.info api and update it in background
|
||||
radioBrowserApi = PreferencesHelper.loadRadioBrowserApiAddress()
|
||||
updateRadioBrowserApi()
|
||||
}
|
||||
|
||||
|
||||
/* Searches station(s) on radio-browser.info */
|
||||
fun searchStation(context: Context, query: String, searchType: Int) {
|
||||
Log.v(TAG, "Search - Querying $radioBrowserApi for: $query")
|
||||
|
||||
// create queue and request
|
||||
requestQueue = Volley.newRequestQueue(context)
|
||||
val requestUrl: String = when (searchType) {
|
||||
// CASE: single station search - by radio browser UUID
|
||||
Keys.SEARCH_TYPE_BY_UUID -> "https://${radioBrowserApi}/json/stations/byuuid/${query}"
|
||||
// CASE: multiple results search by search term
|
||||
else -> "https://${radioBrowserApi}/json/stations/search?name=${query.replace(" ", "+")}"
|
||||
}
|
||||
|
||||
// request data from request URL
|
||||
val stringRequest = object: JsonArrayRequest(Method.GET, requestUrl, null, responseListener, errorListener) {
|
||||
@Throws(AuthFailureError::class)
|
||||
override fun getHeaders(): Map<String, String> {
|
||||
val params = HashMap<String, String>()
|
||||
params["User-Agent"] = "$Keys.APPLICATION_NAME ${BuildConfig.VERSION_NAME}"
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
// override retry policy
|
||||
stringRequest.retryPolicy = object : RetryPolicy {
|
||||
override fun getCurrentTimeout(): Int {
|
||||
return 30000
|
||||
}
|
||||
|
||||
override fun getCurrentRetryCount(): Int {
|
||||
return 30000
|
||||
}
|
||||
|
||||
@Throws(VolleyError::class)
|
||||
override fun retry(error: VolleyError) {
|
||||
Log.w(TAG, "Error: $error")
|
||||
}
|
||||
}
|
||||
|
||||
// add to RequestQueue.
|
||||
requestQueue.add(stringRequest)
|
||||
}
|
||||
|
||||
|
||||
fun stopSearchRequest() {
|
||||
if (this::requestQueue.isInitialized) {
|
||||
requestQueue.stop()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Converts search result JSON string */
|
||||
private fun createRadioBrowserResult(result: String): Array<RadioBrowserResult> {
|
||||
val gsonBuilder = GsonBuilder()
|
||||
gsonBuilder.setDateFormat("M/d/yy hh:mm a")
|
||||
val gson = gsonBuilder.create()
|
||||
return gson.fromJson(result, Array<RadioBrowserResult>::class.java)
|
||||
}
|
||||
|
||||
|
||||
/* Updates the address of the radio-browser.info api */
|
||||
private fun updateRadioBrowserApi() {
|
||||
CoroutineScope(IO).launch {
|
||||
val deferred: Deferred<String> = async { NetworkHelper.getRadioBrowserServerSuspended() }
|
||||
radioBrowserApi = deferred.await()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Listens for (positive) server responses to search requests */
|
||||
private val responseListener: Response.Listener<JSONArray> = Response.Listener<JSONArray> { response ->
|
||||
if (response != null) {
|
||||
radioBrowserSearchListener.onRadioBrowserSearchResults(createRadioBrowserResult(response.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Listens for error response from server */
|
||||
private val errorListener: Response.ErrorListener = Response.ErrorListener { error ->
|
||||
Log.w(TAG, "Error: $error")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* SearchResultAdapter.kt
|
||||
* Implements the SearchResultAdapter class
|
||||
* A SearchResultAdapter is a custom adapter providing search result views for a RecyclerView
|
||||
*
|
||||
* This file is part of
|
||||
* TRANSISTOR - Radio App for Android
|
||||
*
|
||||
* Copyright (c) 2015-22 - Y20K.org
|
||||
* Licensed under the MIT-License
|
||||
* http://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
|
||||
package com.michatec.radio.search
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioFocusRequest
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import com.michatec.radio.R
|
||||
import com.michatec.radio.core.Station
|
||||
|
||||
|
||||
/*
|
||||
* SearchResultAdapter class
|
||||
*/
|
||||
class SearchResultAdapter(
|
||||
private val listener: SearchResultAdapterListener,
|
||||
var searchResults: List<Station>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
/* 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
|
||||
|
||||
/* 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.adapterPosition
|
||||
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.adapterPosition
|
||||
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.adapterPosition]
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
|
||||
// create a new instance of ExoPlayer
|
||||
exoPlayer = ExoPlayer.Builder(context).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
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
|
||||
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
||||
.setAudioAttributes(audioAttributes)
|
||||
.build()
|
||||
|
||||
audioManager.requestAudioFocus(focusRequest)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
// For older versions where AudioFocusRequest is not available
|
||||
audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 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 inner 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)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user