Files
Radio/app/src/main/java/com/michatec/radio/PlayerFragment.kt
2026-01-21 17:10:35 +01:00

810 lines
32 KiB
Kotlin

/*
* PlayerFragment.kt
* Implements the PlayerFragment class
* PlayerFragment is the fragment that hosts Radio's list of stations and a player sheet
*
* 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
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.media.AudioManager
import android.net.Uri
import android.os.*
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionResult
import androidx.media3.session.SessionToken
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.android.volley.Request
import com.android.volley.RequestQueue
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.MaterialTimePicker.INPUT_MODE_KEYBOARD
import com.google.android.material.timepicker.TimeFormat
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.michatec.radio.collection.CollectionAdapter
import com.michatec.radio.collection.CollectionViewModel
import com.michatec.radio.core.Collection
import com.michatec.radio.core.Station
import com.michatec.radio.dialogs.AddStationDialog
import com.michatec.radio.dialogs.FindStationDialog
import com.michatec.radio.dialogs.YesNoDialog
import com.michatec.radio.extensions.*
import com.michatec.radio.helpers.*
import com.michatec.radio.ui.LayoutHolder
import com.michatec.radio.ui.PlayerState
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import java.util.*
/*
* PlayerFragment class
*/
class PlayerFragment : Fragment(),
SharedPreferences.OnSharedPreferenceChangeListener,
FindStationDialog.FindStationDialogListener,
AddStationDialog.AddStationDialogListener,
CollectionAdapter.CollectionAdapterListener,
YesNoDialog.YesNoDialogListener {
/* Define log tag */
private val TAG: String = PlayerFragment::class.java.simpleName
/* Main class variables */
private lateinit var collectionViewModel: CollectionViewModel
private lateinit var layout: LayoutHolder
private lateinit var collectionAdapter: CollectionAdapter
private lateinit var controllerFuture: ListenableFuture<MediaController>
private lateinit var pickSingleMediaLauncher: ActivityResultLauncher<PickVisualMediaRequest>
private lateinit var queue: RequestQueue
private val controller: MediaController?
get() = if (controllerFuture.isDone) controllerFuture.get() else null // defines the Getter for the MediaController
private var collection: Collection = Collection()
private var playerState: PlayerState = PlayerState()
private var listLayoutState: Parcelable? = null
private val handler: Handler = Handler(Looper.getMainLooper())
private var tempStationUuid: String = String()
private var itemTouchHelper: ItemTouchHelper? = null
/* Overrides onCreate from Fragment */
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// handle back tap/gesture
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// minimize player sheet - or if already minimized let activity handle back
if (isEnabled && this@PlayerFragment::layout.isInitialized && !layout.minimizePlayerIfExpanded()) {
isEnabled = false
activity?.onBackPressedDispatcher?.onBackPressed()
}
}
})
queue = Volley.newRequestQueue(requireActivity())
// load player state
playerState = PreferencesHelper.loadPlayerState()
// create view model and observe changes in collection view model
collectionViewModel = ViewModelProvider(this)[CollectionViewModel::class.java]
// create collection adapter
collectionAdapter = CollectionAdapter(
activity as Context,
this as CollectionAdapter.CollectionAdapterListener
)
// restore state of station list
listLayoutState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
savedInstanceState?.getParcelable(Keys.KEY_SAVE_INSTANCE_STATE_STATION_LIST, Parcelable::class.java)
} else {
@Suppress("DEPRECATION")
savedInstanceState?.getParcelable(Keys.KEY_SAVE_INSTANCE_STATE_STATION_LIST)
}
// Initialize single media picker launcher
pickSingleMediaLauncher =
registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { imageUri ->
if (imageUri == null) {
Snackbar.make(requireView(), R.string.toastalert_failed_picking_media, Snackbar.LENGTH_LONG).show()
} else {
collection = CollectionHelper.setStationImageWithStationUuid(
activity as Context,
collection,
imageUri,
tempStationUuid,
imageManuallySet = true
)
tempStationUuid = String()
}
}
Handler(Looper.getMainLooper()).postDelayed({ context?.let { checkForUpdates() } }, 5000)
}
/* Overrides onCreate from Fragment */
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// find views and set them up
val rootView: View = inflater.inflate(R.layout.fragment_player, container, false)
layout = LayoutHolder(rootView)
initializeViews()
// hide action bar
(activity as AppCompatActivity).supportActionBar?.hide()
// set the same background color of the player sheet for the navigation bar
requireActivity().window.navigationBarColor = ContextCompat.getColor(requireActivity(), R.color.player_sheet_background)
// associate the ItemTouchHelper with the RecyclerView
itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback())
itemTouchHelper?.attachToRecyclerView(layout.recyclerView)
return rootView
}
/* Implement the ItemTouchHelper.Callback for drag and drop functionality */
inner class ItemTouchHelperCallback : ItemTouchHelper.Callback() {
override fun isLongPressDragEnabled() = !collectionAdapter.isExpandedForEdit
override fun isItemViewSwipeEnabled() = true
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
// disable drag and drop for the new card
if (viewHolder.itemViewType == Keys.VIEW_TYPE_ADD_NEW) {
return 0
}
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
return makeMovementFlags(dragFlags, swipeFlags)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val fromPosition = viewHolder.bindingAdapterPosition
val toPosition = target.bindingAdapterPosition
collectionAdapter.onItemMove(fromPosition, toPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
collectionAdapter.onItemDismiss(position)
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
collectionAdapter.saveCollectionAfterDragDrop()
}
}
/* Overrides onStart from Fragment */
override fun onStart() {
super.onStart()
// initialize MediaController - connect to PlayerService
initializeController()
}
/* Overrides onSaveInstanceState from Fragment */
override fun onSaveInstanceState(outState: Bundle) {
if (this::layout.isInitialized) {
// save current state of station list
listLayoutState = layout.layoutManager.onSaveInstanceState()
outState.putParcelable(Keys.KEY_SAVE_INSTANCE_STATE_STATION_LIST, listLayoutState)
}
// always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(outState)
}
/* Overrides onResume from Fragment */
override fun onResume() {
super.onResume()
// assign volume buttons to music volume
activity?.volumeControlStream = AudioManager.STREAM_MUSIC
// load player state
playerState = PreferencesHelper.loadPlayerState()
// recreate player ui
// setupPlaybackControls()
updatePlayerViews()
updateStationListState()
togglePeriodicSleepTimerUpdateRequest()
// begin looking for changes in collection
observeCollectionViewModel()
// handle navigation arguments
handleNavigationArguments()
// // handle start intent - if started via tap on rss link
// handleStartIntent()
// start watching for changes in shared preferences
PreferencesHelper.registerPreferenceChangeListener(this as SharedPreferences.OnSharedPreferenceChangeListener)
}
/* Overrides onPause from Fragment */
override fun onPause() {
super.onPause()
// stop receiving playback progress updates
handler.removeCallbacks(periodicSleepTimerUpdateRequestRunnable)
// stop watching for changes in shared preferences
PreferencesHelper.unregisterPreferenceChangeListener(this as SharedPreferences.OnSharedPreferenceChangeListener)
}
/* Overrides onStop from Fragment */
override fun onStop() {
super.onStop()
// release MediaController - cut connection to PlayerService
releaseController()
}
override fun onDestroy() {
super.onDestroy()
queue.cancelAll(TAG)
}
/* Overrides onSharedPreferenceChanged from OnSharedPreferenceChangeListener */
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == Keys.PREF_ACTIVE_DOWNLOADS) {
layout.toggleDownloadProgressIndicator()
}
if (key == Keys.PREF_PLAYER_METADATA_HISTORY) {
requestMetadataUpdate()
}
}
/* Overrides onFindStationDialog from FindStationDialog */
override fun onFindStationDialog(station: Station) {
if (station.streamContent.isNotEmpty() && station.streamContent != Keys.MIME_TYPE_UNSUPPORTED) {
// add station and save collection
collection = CollectionHelper.addStation(activity as Context, collection, station)
} else {
// detect content type on background thread
CoroutineScope(IO).launch {
val contentType: NetworkHelper.ContentType = NetworkHelper.detectContentType(station.getStreamUri())
// set content type
station.streamContent = contentType.type
// add station and save collection
withContext(Main) {
collection = CollectionHelper.addStation(activity as Context, collection, station)
}
}
}
}
/* Overrides onAddStationDialog from AddDialog */
override fun onAddStationDialog(station: Station) {
if (station.streamContent.isNotEmpty() && station.streamContent != Keys.MIME_TYPE_UNSUPPORTED) {
// add station and save collection
collection = CollectionHelper.addStation(activity as Context, collection, station)
}
}
/* Overrides onPlayButtonTapped from CollectionAdapterListener */
override fun onPlayButtonTapped(stationUuid: String) {
// CASE: the selected station is playing
if (controller?.isPlaying == true && stationUuid == playerState.stationUuid) {
// stop playback
controller?.pause()
}
// CASE: the selected station is not playing (another station might be playing)
else {
// start playback
controller?.play(activity as Context, CollectionHelper.getStation(collection, stationUuid))
}
}
/* Overrides onAddNewButtonTapped from CollectionAdapterListener */
override fun onAddNewButtonTapped() {
FindStationDialog(activity as Activity, this as FindStationDialog.FindStationDialogListener).show()
}
/* Overrides onChangeImageButtonTapped from CollectionAdapterListener */
override fun onChangeImageButtonTapped(stationUuid: String) {
tempStationUuid = stationUuid
pickImage()
}
/* Overrides onYesNoDialog from YesNoDialogListener */
override fun onYesNoDialog(
type: Int,
dialogResult: Boolean,
payload: Int,
payloadString: String
) {
super.onYesNoDialog(type, dialogResult, payload, payloadString)
when (type) {
// handle result of remove dialog
Keys.DIALOG_REMOVE_STATION -> {
when (dialogResult) {
// user tapped remove station
true -> collectionAdapter.removeStation(activity as Context, payload)
// user tapped cancel
false -> collectionAdapter.notifyItemChanged(payload)
}
}
// handle result from the restore collection dialog
Keys.DIALOG_RESTORE_COLLECTION -> {
when (dialogResult) {
// user tapped restore
true -> BackupHelper.restore(requireView(), activity as Context, payloadString.toUri())
// user tapped cancel
false -> {
/* do nothing */
}
}
}
}
}
/* Initializes the MediaController - handles connection to PlayerService under the hood */
@OptIn(UnstableApi::class)
private fun initializeController() {
controllerFuture = MediaController.Builder(
activity as Context,
SessionToken(
activity as Context,
ComponentName(activity as Context, PlayerService::class.java)
)
).buildAsync()
controllerFuture.addListener({ setupController() }, MoreExecutors.directExecutor())
}
/* Releases MediaController */
private fun releaseController() {
MediaController.releaseFuture(controllerFuture)
}
/* Sets up the MediaController */
private fun setupController() {
val controller: MediaController = this.controller ?: return
controller.addListener(playerListener)
requestMetadataUpdate()
// handle start intent
handleStartIntent()
}
/* Sets up views and connects tap listeners - first run */
private fun initializeViews() {
// set adapter data source
layout.recyclerView.adapter = collectionAdapter
// enable swipe to delete
val swipeToDeleteHandler = object : UiHelper.SwipeToDeleteCallback(activity as Context) {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// ask user
val bindingAdapterPosition: Int = viewHolder.bindingAdapterPosition
val dialogMessage =
"${getString(R.string.dialog_yes_no_message_remove_station)}\n\n- ${collection.stations[bindingAdapterPosition].name}"
YesNoDialog(this@PlayerFragment as YesNoDialog.YesNoDialogListener).show(
context = activity as Context,
type = Keys.DIALOG_REMOVE_STATION,
messageString = dialogMessage,
yesButton = R.string.dialog_yes_no_positive_button_remove_station,
payload = bindingAdapterPosition
)
}
}
val swipeToDeleteItemTouchHelper = ItemTouchHelper(swipeToDeleteHandler)
swipeToDeleteItemTouchHelper.attachToRecyclerView(layout.recyclerView)
// enable swipe to mark starred
val swipeToMarkStarredHandler =
object : UiHelper.SwipeToMarkStarredCallback(activity as Context) {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// mark card starred
val bindingAdapterPosition: Int = viewHolder.bindingAdapterPosition
collectionAdapter.toggleStarredStation(activity as Context, bindingAdapterPosition)
}
}
val swipeToMarkStarredItemTouchHelper = ItemTouchHelper(swipeToMarkStarredHandler)
swipeToMarkStarredItemTouchHelper.attachToRecyclerView(layout.recyclerView)
// set up sleep timer start button
layout.sheetSleepTimerStartButtonView.setOnClickListener {
when (controller?.isPlaying) {
true -> {
val timePicker = MaterialTimePicker.Builder()
.setTimeFormat(TimeFormat.CLOCK_24H)
.setHour(0)
.setMinute(1)
.setInputMode(INPUT_MODE_KEYBOARD)
.build()
timePicker.addOnPositiveButtonClickListener {
val selectedTimeMillis = (timePicker.hour * 60 * 60 * 1000L) + (timePicker.minute * 60 * 1000L) + 1000
// start the sleep timer with the selected time
playerState.sleepTimerRunning = true
controller?.startSleepTimer(selectedTimeMillis)
togglePeriodicSleepTimerUpdateRequest()
}
// display the TimePicker dialog
timePicker.show(requireActivity().supportFragmentManager, "tag")
}
else -> Snackbar.make(
requireView(),
R.string.toastmessage_sleep_timer_unable_to_start,
Snackbar.LENGTH_SHORT
).show()
}
}
// set up sleep timer cancel button
layout.sheetSleepTimerCancelButtonView.setOnClickListener {
playerState.sleepTimerRunning = false
controller?.cancelSleepTimer()
togglePeriodicSleepTimerUpdateRequest()
}
}
/* Sets up the player */
private fun updatePlayerViews() {
// get station
var station = Station()
if (playerState.stationUuid.isNotEmpty()) {
// get station from player state
station = CollectionHelper.getStation(collection, playerState.stationUuid)
} else if (collection.stations.isNotEmpty()) {
// fallback: get first station
station = collection.stations[0]
playerState.stationUuid = station.uuid
}
// update views
layout.togglePlayButton(playerState.isPlaying)
layout.updatePlayerViews(activity as Context, station, playerState.isPlaying)
// main play/pause button
layout.playButtonView.setOnClickListener {
onPlayButtonTapped(playerState.stationUuid)
}
}
/* Sets up state of list station list */
private fun updateStationListState() {
if (listLayoutState != null) {
layout.layoutManager.onRestoreInstanceState(listLayoutState)
}
}
/* Requests an update of the sleep timer from the player service */
private fun requestSleepTimerUpdate() {
val resultFuture: ListenableFuture<SessionResult>? =
controller?.requestSleepTimerRemaining()
resultFuture?.addListener({
val timeRemaining: Long = resultFuture.get().extras.getLong(Keys.EXTRA_SLEEP_TIMER_REMAINING)
layout.updateSleepTimer(activity as Context, timeRemaining)
}, MoreExecutors.directExecutor())
}
/* Requests an update of the metadata history from the player service */
private fun requestMetadataUpdate() {
val resultFuture: ListenableFuture<SessionResult>? = controller?.requestMetadataHistory()
resultFuture?.addListener({
val metadata: ArrayList<String>? = resultFuture.get().extras.getStringArrayList(Keys.EXTRA_METADATA_HISTORY)
layout.updateMetadata(metadata?.toMutableList())
}, MoreExecutors.directExecutor())
}
/* Start image picker */
private fun pickImage() {
pickSingleMediaLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
/* Handles this activity's start intent */
private fun handleStartIntent() {
if ((activity as Activity).intent.action != null) {
when ((activity as Activity).intent.action) {
Keys.ACTION_SHOW_PLAYER -> handleShowPlayer()
Intent.ACTION_VIEW -> handleViewIntent()
Keys.ACTION_START -> handleStartPlayer()
}
}
// clear intent action to prevent double calls
(activity as Activity).intent.action = ""
}
/* Handles ACTION_SHOW_PLAYER request from notification */
private fun handleShowPlayer() {
Log.i(TAG, "Tap on notification registered.")
layout.showPlayer(requireActivity())
}
/* Handles ACTION_VIEW request to add Station */
private fun handleViewIntent() {
val intentUri: Uri? = (activity as Activity).intent.data
if (intentUri != null) {
CoroutineScope(IO).launch {
// get station list from intent source
val stationList: MutableList<Station> = mutableListOf()
val scheme: String = intentUri.scheme ?: String()
// CASE: intent is a web link
if (scheme.startsWith("http")) {
Log.i(TAG, "Transistor was started to handle a web link.")
stationList.addAll(CollectionHelper.createStationsFromUrl(intentUri.toString()))
}
// CASE: intent is a local file
else if (scheme.startsWith("content")) {
Log.i(TAG, "Transistor was started to handle a local audio playlist.")
stationList.addAll(CollectionHelper.createStationListFromContentUri(activity as Context, intentUri))
}
withContext(Main) {
if (stationList.isNotEmpty()) {
AddStationDialog(activity as Activity, stationList, this@PlayerFragment as AddStationDialog.AddStationDialogListener).show()
} else {
// invalid address
Toast.makeText(context, R.string.toastmessage_station_not_valid, Toast.LENGTH_LONG).show()
}
}
}
}
}
/* Handles START_PLAYER_SERVICE request from App Shortcut */
private fun handleStartPlayer() {
val intent: Intent = (activity as Activity).intent
if (intent.hasExtra(Keys.EXTRA_START_LAST_PLAYED_STATION)) {
controller?.play(activity as Context, CollectionHelper.getStation(collection, playerState.stationUuid))
} else if (intent.hasExtra(Keys.EXTRA_STATION_UUID)) {
val uuid: String = intent.getStringExtra(Keys.EXTRA_STATION_UUID) ?: String()
controller?.play(activity as Context, CollectionHelper.getStation(collection, uuid))
} else if (intent.hasExtra(Keys.EXTRA_STREAM_URI)) {
val streamUri: String = intent.getStringExtra(Keys.EXTRA_STREAM_URI) ?: String()
controller?.playStreamDirectly(streamUri)
}
}
/* Toggle periodic update request of Sleep Timer state from player service */
private fun togglePeriodicSleepTimerUpdateRequest() {
handler.removeCallbacks(periodicSleepTimerUpdateRequestRunnable)
handler.postDelayed(periodicSleepTimerUpdateRequestRunnable, 0)
}
/* Observe view model of collection of stations */
private fun observeCollectionViewModel() {
collectionViewModel.collectionLiveData.observe(this) {
// update collection
collection = it
//// // updates current station in player views
//// playerState = PreferencesHelper.loadPlayerState()
// // get station
// val station: Station = CollectionHelper.getStation(collection, playerState.stationUuid)
// // update player views
// layout.updatePlayerViews(activity as Context, station, playerState.isPlaying)
//// // handle start intent
//// handleStartIntent()
//// // handle navigation arguments
//// handleNavigationArguments()
}
collectionViewModel.collectionSizeLiveData.observe(this) {
// size of collection changed
layout.toggleOnboarding(activity as Context, collection.stations.size)
updatePlayerViews()
CollectionHelper.exportCollectionM3u(activity as Context, collection)
CollectionHelper.exportCollectionPls(activity as Context, collection)
}
}
/* Handles arguments handed over by navigation (from SettingsFragment) */
private fun handleNavigationArguments() {
// get arguments
val updateCollection: Boolean =
arguments?.getBoolean(Keys.ARG_UPDATE_COLLECTION, false) ?: false
val updateStationImages: Boolean =
arguments?.getBoolean(Keys.ARG_UPDATE_IMAGES, false) ?: false
val restoreCollectionFileString: String? = arguments?.getString(Keys.ARG_RESTORE_COLLECTION)
if (updateCollection) {
arguments?.putBoolean(Keys.ARG_UPDATE_COLLECTION, false)
val updateHelper = UpdateHelper(activity as Context, collectionAdapter, collection)
updateHelper.updateCollection()
}
if (updateStationImages) {
arguments?.putBoolean(Keys.ARG_UPDATE_IMAGES, false)
DownloadHelper.updateStationImages(activity as Context)
}
if (!restoreCollectionFileString.isNullOrEmpty()) {
arguments?.putString(Keys.ARG_RESTORE_COLLECTION, null)
when (collection.stations.isNotEmpty()) {
true -> {
YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(
context = activity as Context,
type = Keys.DIALOG_RESTORE_COLLECTION,
messageString = getString(R.string.dialog_restore_collection_replace_existing),
payloadString = restoreCollectionFileString
)
}
false -> {
BackupHelper.restore(
requireView(),
activity as Context,
restoreCollectionFileString.toUri()
)
}
}
}
}
/*
* Runnable: Periodically requests sleep timer state
*/
private val periodicSleepTimerUpdateRequestRunnable: Runnable = object : Runnable {
override fun run() {
// update sleep timer view
requestSleepTimerUpdate()
// use the handler to start runnable again after specified delay
handler.postDelayed(this, 500)
}
}
/*
* End of declaration
*/
/*
* Player.Listener: Called when one or more player states changed.
*/
private var playerListener: Player.Listener = object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
// store new station
playerState.stationUuid = mediaItem?.mediaId ?: String()
// update station specific views
updatePlayerViews()
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
// store state of playback
playerState.isPlaying = isPlaying
// animate state transition of play button(s)
layout.animatePlaybackButtonStateTransition(activity as Context, isPlaying)
if (isPlaying) {
// playback is active
layout.showPlayer(activity as Context)
layout.showBufferingIndicator(buffering = false)
} else {
// playback is paused or stopped
// check if buffering (playback is not active but playWhenReady is true)
if (controller?.playWhenReady == true) {
// playback is buffering, show the buffering indicator
layout.showBufferingIndicator(buffering = true)
} else {
// playback is not buffering, hide the buffering indicator
layout.showBufferingIndicator(buffering = false)
}
}
}
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
if (playWhenReady && controller?.isPlaying == false) {
layout.showBufferingIndicator(buffering = true)
}
}
override fun onPlayerError(error: PlaybackException) {
super.onPlayerError(error)
layout.togglePlayButton(false)
layout.showBufferingIndicator(false)
Toast.makeText(activity, R.string.toastmessage_connection_failed, Toast.LENGTH_LONG).show()
}
}
/*
* Check for update on github
*/
private fun checkForUpdates() {
val url = getString(R.string.snackbar_github_update_check_url)
val request = StringRequest(Request.Method.GET, url, { reply ->
val latestVersion = Gson().fromJson(reply, JsonObject::class.java).get("tag_name").asString
val current = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity?.packageManager?.getPackageInfo(requireActivity().packageName, PackageManager.PackageInfoFlags.of(0))?.versionName
} else {
activity?.packageManager?.getPackageInfo(requireActivity().packageName, 0)?.versionName
}
if (latestVersion != current) {
// We have an update available, tell our user about it
view?.let {
Snackbar.make(it, getString(R.string.app_name) + " " + latestVersion + " " + getString(R.string.snackbar_update_available), 10000)
.setAction(R.string.snackbar_show) {
val releaseurl = getString(R.string.snackbar_url_app_home_page)
val i = Intent(Intent.ACTION_VIEW)
i.data = releaseurl.toUri()
// Not sure that does anything
i.putExtra("SOURCE", "SELF")
startActivity(i)
}
.setActionTextColor(
ContextCompat.getColor(
requireActivity(),
R.color.default_neutral_white))
.show()
}
}
}, { error ->
Log.w(TAG, "Update check failed", error)
})
request.tag = TAG
queue.add(request)
}
}