mirror of
https://github.com/Michatec/Radio.git
synced 2026-01-31 07:20:40 +00:00
810 lines
32 KiB
Kotlin
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)
|
|
}
|
|
}
|