/* * LayoutHolder.kt * Implements the LayoutHolder class * A LayoutHolder hold references to the main views * * 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.ui import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.graphics.drawable.AnimatedVectorDrawable import android.os.Build import android.view.View import android.view.animation.Animation import android.view.animation.AnimationUtils import android.widget.ImageButton import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.Group import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.snackbar.Snackbar import com.michatec.radio.Keys import com.michatec.radio.R import com.michatec.radio.core.Station import com.michatec.radio.helpers.DateTimeHelper import com.michatec.radio.helpers.ImageHelper import com.michatec.radio.helpers.PreferencesHelper import com.michatec.radio.helpers.UiHelper /* * LayoutHolder class */ data class LayoutHolder(var rootView: View) { /* Main class variables */ var recyclerView: RecyclerView = rootView.findViewById(R.id.station_list) val layoutManager: LinearLayoutManager private var bottomSheet: ConstraintLayout = rootView.findViewById(R.id.bottom_sheet) //private var sheetMetadataViews: Group private var sleepTimerRunningViews: Group = rootView.findViewById(R.id.sleep_timer_running_views) private var downloadProgressIndicator: ProgressBar = rootView.findViewById(R.id.download_progress_indicator) private var stationImageView: ImageView = rootView.findViewById(R.id.station_icon) private var stationNameView: TextView = rootView.findViewById(R.id.player_station_name) private var metadataView: TextView = rootView.findViewById(R.id.player_station_metadata) var playButtonView: ImageButton = rootView.findViewById(R.id.player_play_button) private var bufferingIndicator: ProgressBar = rootView.findViewById(R.id.player_buffering_indicator) private var sheetStreamingLinkHeadline: TextView = rootView.findViewById(R.id.sheet_streaming_link_headline) private var sheetStreamingLinkView: TextView = rootView.findViewById(R.id.sheet_streaming_link) private var sheetMetadataHistoryHeadline: TextView = rootView.findViewById(R.id.sheet_metadata_headline) private var sheetMetadataHistoryView: TextView = rootView.findViewById(R.id.sheet_metadata_history) private var sheetNextMetadataView: ImageButton = rootView.findViewById(R.id.sheet_next_metadata_button) private var sheetPreviousMetadataView: ImageButton = rootView.findViewById(R.id.sheet_previous_metadata_button) private var sheetCopyMetadataButtonView: ImageButton = rootView.findViewById(R.id.copy_station_metadata_button) private var sheetShareLinkButtonView: ImageView = rootView.findViewById(R.id.sheet_share_link_button) private var sheetBitrateView: TextView = rootView.findViewById(R.id.sheet_bitrate_view) var sheetSleepTimerStartButtonView: ImageButton = rootView.findViewById(R.id.sleep_timer_start_button) var sheetSleepTimerCancelButtonView: ImageButton = rootView.findViewById(R.id.sleep_timer_cancel_button) private var sheetSleepTimerRemainingTimeView: TextView = rootView.findViewById(R.id.sleep_timer_remaining_time) private var onboardingLayout: ConstraintLayout = rootView.findViewById(R.id.onboarding_layout) private var bottomSheetBehavior: BottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) private var metadataHistory: MutableList private var metadataHistoryPosition: Int private var isBuffering: Boolean /* Init block */ init { // find views //sheetMetadataViews = rootView.findViewById(R.id.sheet_metadata_views) metadataHistory = PreferencesHelper.loadMetadataHistory() metadataHistoryPosition = metadataHistory.size - 1 isBuffering = false // set up RecyclerView layoutManager = CustomLayoutManager(rootView.context) recyclerView.layoutManager = layoutManager recyclerView.itemAnimator = DefaultItemAnimator() // set up metadata history next and previous buttons sheetPreviousMetadataView.setOnClickListener { if (metadataHistory.isNotEmpty()) { if (metadataHistoryPosition > 0) { metadataHistoryPosition -= 1 } else { metadataHistoryPosition = metadataHistory.size - 1 } sheetMetadataHistoryView.text = metadataHistory[metadataHistoryPosition] } } sheetNextMetadataView.setOnClickListener { if (metadataHistory.isNotEmpty()) { if (metadataHistoryPosition < metadataHistory.size - 1) { metadataHistoryPosition += 1 } else { metadataHistoryPosition = 0 } sheetMetadataHistoryView.text = metadataHistory[metadataHistoryPosition] } } sheetMetadataHistoryView.setOnLongClickListener { copyMetadataHistoryToClipboard() return@setOnLongClickListener true } sheetMetadataHistoryHeadline.setOnLongClickListener { copyMetadataHistoryToClipboard() return@setOnLongClickListener true } // set layout for player setupBottomSheet() } /* Updates the player views */ @SuppressLint("DefaultLocale") fun updatePlayerViews(context: Context, station: Station, isPlaying: Boolean) { // set default metadata views, when playback has stopped if (!isPlaying) { metadataView.text = station.name sheetMetadataHistoryView.text = station.name // sheetMetadataHistoryView.isSelected = true } // update name stationNameView.text = station.name // toggle text scrolling (marquee) if necessary stationNameView.isSelected = isPlaying // reduce the shadow left and right because of scrolling (Marquee) stationNameView.setFadingEdgeLength(8) // update cover if (station.imageColor != -1) { stationImageView.setBackgroundColor(station.imageColor) } stationImageView.setImageBitmap(ImageHelper.getStationImage(context, station.smallImage)) stationImageView.contentDescription = "${context.getString(R.string.descr_player_station_image)}: ${station.name}" // update streaming link sheetStreamingLinkView.text = station.getStreamUri() val bitrateText: CharSequence = if (station.codec.isNotEmpty()) { if (station.bitrate == 0) { // show only the codec when the bitrate is at "0" from radio-browser.info API station.codec } else { val kiloBytesPerSecond = station.bitrate / 8F val dataRateString = if (kiloBytesPerSecond >= 1000) { String.format("%.2f mb/s", kiloBytesPerSecond / 1000F) } else { String.format("%.0f kb/s", kiloBytesPerSecond) } // show the bitrate and codec if the result is available in the radio-browser.info API buildString { append(station.codec) append(" | ") append(station.bitrate) append("kbps") append(" | ") append(dataRateString) } } } else { // do not show for M3U and PLS playlists as they do not include codec or bitrate "" } // update bitrate sheetBitrateView.text = bitrateText // update click listeners sheetStreamingLinkHeadline.setOnClickListener { copyToClipboard( context, sheetStreamingLinkView.text ) } sheetStreamingLinkView.setOnClickListener { copyToClipboard( context, sheetStreamingLinkView.text ) } sheetMetadataHistoryHeadline.setOnClickListener { copyToClipboard( context, sheetMetadataHistoryView.text ) } sheetMetadataHistoryView.setOnClickListener { copyToClipboard( context, sheetMetadataHistoryView.text ) } sheetCopyMetadataButtonView.setOnClickListener { copyToClipboard( context, sheetMetadataHistoryView.text ) } sheetBitrateView.setOnClickListener { copyToClipboard( context, sheetBitrateView.text ) } sheetShareLinkButtonView.setOnClickListener { val share = Intent.createChooser(Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TITLE, stationNameView.text) putExtra(Intent.EXTRA_TEXT, sheetStreamingLinkView.text) type = "text/plain" }, null) context.startActivity(share) } } /* Copies given string to clipboard */ private fun copyToClipboard(context: Context, clipString: CharSequence) { val clip: ClipData = ClipData.newPlainText("simple text", clipString) val cm: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(clip) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { // since API 33 (TIRAMISU) the OS displays its own notification when content is copied to the clipboard Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show() } } /* Copies collected metadata to clipboard */ private fun copyMetadataHistoryToClipboard() { val metadataHistory: MutableList = PreferencesHelper.loadMetadataHistory() val stringBuilder: StringBuilder = StringBuilder() metadataHistory.forEach { stringBuilder.append("${it.trim()}\n") } copyToClipboard(rootView.context, stringBuilder.toString()) } /* Updates the metadata views */ fun updateMetadata(metadataHistoryList: MutableList?) { if (!metadataHistoryList.isNullOrEmpty()) { metadataHistory = metadataHistoryList if (metadataHistory.last() != metadataView.text) { metadataHistoryPosition = metadataHistory.size - 1 val metadataString = metadataHistory[metadataHistoryPosition] metadataView.text = metadataString sheetMetadataHistoryView.text = metadataString } } } /* Updates sleep timer views */ fun updateSleepTimer(context: Context, timeRemaining: Long = 0L) { when (timeRemaining) { 0L -> { sleepTimerRunningViews.isGone = true } else -> { sleepTimerRunningViews.isVisible = true val sleepTimerTimeRemaining = DateTimeHelper.convertToHoursMinutesSeconds(timeRemaining) sheetSleepTimerRemainingTimeView.text = sleepTimerTimeRemaining sheetSleepTimerRemainingTimeView.contentDescription = "${context.getString(R.string.descr_expanded_player_sleep_timer_remaining_time)}: $sleepTimerTimeRemaining" stationNameView.isSelected = false } } } /* Toggles play/pause button */ fun togglePlayButton(isPlaying: Boolean) { if (isPlaying) { playButtonView.setImageResource(R.drawable.ic_audio_waves_animated) val animatedVectorDrawable = playButtonView.drawable as? AnimatedVectorDrawable animatedVectorDrawable?.start() sheetSleepTimerStartButtonView.isVisible = true // bufferingIndicator.isVisible = false } else { playButtonView.setImageResource(R.drawable.ic_player_play_symbol_42dp) sheetSleepTimerStartButtonView.isVisible = false // bufferingIndicator.isVisible = isBuffering } } /* Toggles buffering indicator */ fun showBufferingIndicator(buffering: Boolean) { bufferingIndicator.isVisible = buffering isBuffering = buffering } /* Toggles visibility of player depending on playback state - hiding it when playback is stopped (not paused or playing) */ // fun togglePlayerVisibility(context: Context, playbackState: Int): Boolean { // when (playbackState) { // PlaybackStateCompat.STATE_STOPPED -> return hidePlayer(context) // PlaybackStateCompat.STATE_NONE -> return hidePlayer(context) // PlaybackStateCompat.STATE_ERROR -> return hidePlayer(context) // else -> return showPlayer(context) // } // } /* Toggles visibility of the download progress indicator */ fun toggleDownloadProgressIndicator() { when (PreferencesHelper.loadActiveDownloads()) { Keys.ACTIVE_DOWNLOADS_EMPTY -> downloadProgressIndicator.isGone = true else -> downloadProgressIndicator.isVisible = true } } /* Toggles visibility of the onboarding screen */ fun toggleOnboarding(context: Context, collectionSize: Int): Boolean { return if (collectionSize == 0 && PreferencesHelper.loadCollectionSize() <= 0) { onboardingLayout.isVisible = true hidePlayer(context) true } else { onboardingLayout.isGone = true showPlayer(context) false } } /* Initiates the rotation animation of the play button */ fun animatePlaybackButtonStateTransition(context: Context, isPlaying: Boolean) { when (isPlaying) { true -> { val rotateClockwise = AnimationUtils.loadAnimation(context, R.anim.rotate_clockwise_slow) rotateClockwise.setAnimationListener(createAnimationListener(true)) playButtonView.startAnimation(rotateClockwise) } false -> { val rotateCounterClockwise = AnimationUtils.loadAnimation(context, R.anim.rotate_counterclockwise_fast) rotateCounterClockwise.setAnimationListener(createAnimationListener(false)) playButtonView.startAnimation(rotateCounterClockwise) } } } /* Shows player */ fun showPlayer(context: Context): Boolean { UiHelper.setViewMargins(context, recyclerView, 0, 0, 0, Keys.BOTTOM_SHEET_PEEK_HEIGHT) if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_HIDDEN && onboardingLayout.isGone) { bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED } return true } /* Hides player */ private fun hidePlayer(context: Context): Boolean { UiHelper.setViewMargins(context, recyclerView, 0, 0, 0, 0) bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN return true } /* Minimizes player sheet if expanded */ fun minimizePlayerIfExpanded(): Boolean { return if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED) { bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED true } else { false } } /* Creates AnimationListener for play button */ private fun createAnimationListener(isPlaying: Boolean): Animation.AnimationListener { return object : Animation.AnimationListener { override fun onAnimationStart(animation: Animation) {} override fun onAnimationEnd(animation: Animation) { // set up button symbol and playback indicator afterwards togglePlayButton(isPlaying) } override fun onAnimationRepeat(animation: Animation) {} } } /* Sets up the player (BottomSheet) */ private fun setupBottomSheet() { // show / hide the small player bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onSlide(view: View, slideOffset: Float) { } override fun onStateChanged(view: View, state: Int) { when (state) { BottomSheetBehavior.STATE_COLLAPSED -> Unit // do nothing BottomSheetBehavior.STATE_DRAGGING -> Unit // do nothing BottomSheetBehavior.STATE_EXPANDED -> Unit // do nothing BottomSheetBehavior.STATE_HALF_EXPANDED -> Unit // do nothing BottomSheetBehavior.STATE_SETTLING -> Unit // do nothing BottomSheetBehavior.STATE_HIDDEN -> showPlayer(rootView.context) } } }) // toggle collapsed state on tap bottomSheet.setOnClickListener { toggleBottomSheetState() } stationImageView.setOnClickListener { toggleBottomSheetState() } stationNameView.setOnClickListener { toggleBottomSheetState() } metadataView.setOnClickListener { toggleBottomSheetState() } } /* Toggle expanded/collapsed state of bottom sheet */ private fun toggleBottomSheetState() { when (bottomSheetBehavior.state) { BottomSheetBehavior.STATE_COLLAPSED -> bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED else -> bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED } } /* * Inner class: Custom LinearLayoutManager */ private class CustomLayoutManager(context: Context) : LinearLayoutManager(context, VERTICAL, false) { override fun supportsPredictiveItemAnimations(): Boolean { return true } } /* * End of inner class */ }