Leanback: split DetailsFragment into separate Fragments for Events and Streams

This commit is contained in:
Felix 2020-04-10 16:59:21 +02:00
parent 4e8d5fb371
commit c2be295029
12 changed files with 370 additions and 104 deletions

View file

@ -130,6 +130,7 @@ dependencies {
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
api "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
api "androidx.core:core-ktx:1.2.0"
api 'androidx.recyclerview:recyclerview:1.1.0'

View file

@ -2,6 +2,7 @@ package de.nicidienase.chaosflix.common
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Event
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Recording
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.Stream
object ChaosflixUtil {
fun getOptimalRecording(recordings: List<Recording>, originalLanguage: String): Recording {
@ -28,6 +29,14 @@ object ChaosflixUtil {
}
}
fun getStringForRecording(recording: Recording): String {
return "${if (recording.isHighQuality) "HD" else "SD"} ${recording.folder} [${recording.language}]"
}
fun getStringForStream(stream: Stream): String {
return "${stream.display}"
}
private fun getRecordingForGroup(group: List<Recording>?, language: String): Recording {
if (group.isNullOrEmpty()) {
error("Got empty or null list, this should not happen!")

View file

@ -1,9 +0,0 @@
package de.nicidienase.chaosflix.common.util
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Recording
object RecordingUtil {
fun getStringForRecording(recording: Recording): String {
return "${if (recording.isHighQuality) "HD" else "SD"} ${recording.folder} [${recording.language}]"
}
}

View file

@ -18,7 +18,7 @@ class BrowseErrorFragment : ErrorSupportFragment() {
}
spinnerFragment = SpinnerFragment()
spinnerFragment?.let {
fragmentManager?.beginTransaction()?.add(fragmentId, it)?.commit()
parentFragmentManager?.beginTransaction()?.add(fragmentId, it)?.commit()
}
}
@ -26,19 +26,19 @@ class BrowseErrorFragment : ErrorSupportFragment() {
setErrorContent(resources.getString(resourceId))
}
fun setErrorContent(message: String, fragmentManager: androidx.fragment.app.FragmentManager? = activity?.supportFragmentManager) {
fun setErrorContent(message: String, parentFragmentManager: androidx.fragment.app.FragmentManager? = activity?.supportFragmentManager) {
try {
if (!isDetached) {
spinnerFragment?.let {
fragmentManager?.beginTransaction()?.remove(it)?.commit()
parentFragmentManager?.beginTransaction()?.remove(it)?.commit()
}
imageDrawable = resources.getDrawable(R.drawable.lb_ic_sad_cloud, null)
setMessage(message)
setDefaultBackground(TRANSLUCENT)
buttonText = resources.getString(R.string.dismiss_error)
if (fragmentManager != null) {
setButtonClickListener { _ -> dismiss(fragmentManager) }
if (parentFragmentManager != null) {
setButtonClickListener { _ -> dismiss(parentFragmentManager) }
} else {
setButtonClickListener { _ -> dismiss() }
}
@ -51,22 +51,22 @@ class BrowseErrorFragment : ErrorSupportFragment() {
override fun onPause() {
super.onPause()
spinnerFragment?.let {
fragmentManager?.beginTransaction()?.remove(it)?.commit()
parentFragmentManager?.beginTransaction()?.remove(it)?.commit()
} ?: Log.e(TAG, "Could not remove spinnerFragment")
}
fun dismiss(fragmentManager: androidx.fragment.app.FragmentManager? = activity?.supportFragmentManager) {
if (fragmentManager != null) {
with(fragmentManager.beginTransaction()) {
fun dismiss(parentFragmentManager: androidx.fragment.app.FragmentManager? = activity?.supportFragmentManager) {
if (parentFragmentManager != null) {
with(parentFragmentManager.beginTransaction()) {
spinnerFragment?.let {
remove(it)
}
remove(this@BrowseErrorFragment)
commit()
fragmentManager.popBackStack()
parentFragmentManager.popBackStack()
}
} else {
Log.e(TAG, "Cannot dismiss, fragmentManager is null")
Log.e(TAG, "Cannot dismiss, parentFragmentManager is null")
}
}

View file

@ -12,7 +12,7 @@ import androidx.leanback.widget.ListRowPresenter
import androidx.leanback.widget.Row
import androidx.leanback.widget.SectionRow
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.ViewModelProvider
import de.nicidienase.chaosflix.common.mediadata.MediaRepository
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Conference
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.ConferenceGroup
@ -65,7 +65,7 @@ class ConferencesBrowseFragment : BrowseSupportFragment() {
title = resources.getString(R.string.app_name)
badgeDrawable = resources.getDrawable(R.drawable.chaosflix_icon, null)
viewModel = ViewModelProviders.of(this, ViewModelFactory.getInstance(requireContext())).get(BrowseViewModel::class.java)
viewModel = ViewModelProvider(this, ViewModelFactory.getInstance(requireContext())).get(BrowseViewModel::class.java)
// Recomendation Rows and Adapter
watchListAdapter = ChaosflixEventAdapter(eventPresenter)
@ -102,7 +102,7 @@ class ConferencesBrowseFragment : BrowseSupportFragment() {
viewModel.getConferenceGroups().observe(viewLifecycleOwner, Observer { conferenceGroups ->
if (conferenceGroups != null && conferenceGroups.isNotEmpty()) {
val conferenceRows = ArrayList<Row>()
errorFragment?.dismiss(fragmentManager)
errorFragment?.dismiss(parentFragmentManager)
errorFragment = null
for (group in conferenceGroups.sorted()) {
var row = conferencesGroupRows.get(group.name)
@ -122,16 +122,16 @@ class ConferencesBrowseFragment : BrowseSupportFragment() {
when (downloaderEvent?.state) {
MediaRepository.State.RUNNING -> {
Log.i(TAG, "Refresh running")
fragmentManager?.let {
parentFragmentManager?.let {
errorFragment = BrowseErrorFragment.showErrorFragment(it, R.id.browse_fragment)
}
}
MediaRepository.State.DONE -> {
if (downloaderEvent.error != null) {
val errorMessage = downloaderEvent.error ?: "Error refreshing events"
errorFragment?.setErrorContent(errorMessage, fragmentManager)
errorFragment?.setErrorContent(errorMessage, parentFragmentManager)
} else {
errorFragment?.dismiss(fragmentManager)
errorFragment?.dismiss(parentFragmentManager)
errorFragment = null
}
}

View file

@ -4,7 +4,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.WindowManager
import androidx.fragment.app.FragmentActivity
import androidx.core.os.bundleOf
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Event
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.Room
import de.nicidienase.chaosflix.leanback.R
@ -20,6 +20,16 @@ class DetailsActivity : androidx.fragment.app.FragmentActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_event_details)
val fragment = when(intent.getIntExtra(TYPE, 0)) {
TYPE_RECORDING -> EventDetailsFragment().apply {
arguments = bundleOf(EVENT to intent.getParcelableExtra<Event>(EVENT) )
}
TYPE_STREAM -> StreamDetailsFragment().apply {
arguments = bundleOf(ROOM to intent.getParcelableExtra<Room>(ROOM))
}
else -> error("undefinded type")
}
supportFragmentManager.beginTransaction().replace(R.id.details_fragment_container, fragment).commit()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}

View file

@ -52,11 +52,10 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
import com.google.android.exoplayer2.upstream.HttpDataSource
import com.google.android.exoplayer2.util.Util
import de.nicidienase.chaosflix.common.ChaosflixUtil
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Event
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Recording
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.Room
import de.nicidienase.chaosflix.common.mediadata.network.ApiFactory
import de.nicidienase.chaosflix.common.util.RecordingUtil
import de.nicidienase.chaosflix.common.viewmodel.DetailsViewModel
import de.nicidienase.chaosflix.common.viewmodel.PlayerViewModel
import de.nicidienase.chaosflix.common.viewmodel.ViewModelFactory
@ -79,8 +78,7 @@ class EventDetailsFragment : DetailsSupportFragment() {
private lateinit var detailsViewModel: DetailsViewModel
private lateinit var playerViewModel: PlayerViewModel
private var event: Event? = null
private var room: Room? = null
private lateinit var event: Event
private lateinit var rowsAdapter: ArrayObjectAdapter
@ -104,17 +102,8 @@ class EventDetailsFragment : DetailsSupportFragment() {
detailsViewModel = ViewModelProvider(this, viewModelFactory).get(DetailsViewModel::class.java)
playerViewModel = ViewModelProvider(this, viewModelFactory).get(PlayerViewModel::class.java)
event = activity?.intent?.getParcelableExtra(DetailsActivity.EVENT)
room = activity?.intent?.getParcelableExtra(DetailsActivity.ROOM)
val eventType =
when {
event != null -> DetailsActivity.TYPE_RECORDING
room != null -> DetailsActivity.TYPE_STREAM
else -> -1
}
title = event?.title
event = arguments?.getParcelable(DetailsActivity.EVENT) ?: error("Missing Argument Event")
title = event.title
val selector = ClassPresenterSelector()
val detailsPresenter = FullWidthDetailsOverviewRowPresenter(
@ -134,16 +123,13 @@ class EventDetailsFragment : DetailsSupportFragment() {
detailsBackgroundController.enableParallax()
playerGlue = buildPlayerGlue()
playerGlue.title = event?.title ?: room?.display
playerGlue.subtitle = event?.subtitle ?: ""
playerGlue.title = event.title
playerGlue.subtitle = event.subtitle ?: ""
detailsBackgroundController.setupVideoPlayback(playerGlue)
onItemViewClickedListener = ItemViewClickedListener(this)
when (eventType) {
DetailsActivity.TYPE_RECORDING -> event?.let { onCreateRecording(it, rowsAdapter) }
DetailsActivity.TYPE_STREAM -> room?.let { onCreateStream(it, rowsAdapter) }
}
onCreateRecording(event, rowsAdapter)
adapter = this.rowsAdapter
startEntranceTransition()
@ -224,12 +210,12 @@ class EventDetailsFragment : DetailsSupportFragment() {
}
}
private fun selectRecordingFromList(event: Event, items: List<Recording>, resultHandler: (Recording) -> Unit) {
val onClickListener = DialogInterface.OnClickListener { _, which -> resultHandler.invoke(items[which]) }
private fun selectRecordingFromList(event: Event, recordings: List<Recording>, resultHandler: (Recording) -> Unit) {
val onClickListener = DialogInterface.OnClickListener { _, which -> resultHandler.invoke(recordings[which]) }
if (selectDialog != null) {
selectDialog?.dismiss()
}
val strings = items.map { RecordingUtil.getStringForRecording(it) }.toTypedArray()
val strings = recordings.map { ChaosflixUtil.getStringForRecording(it) }.toTypedArray()
selectDialog = AlertDialog.Builder(requireContext())
.setItems(strings, onClickListener)
.setNegativeButton("Autoselect") { _, _ ->
@ -293,31 +279,9 @@ class EventDetailsFragment : DetailsSupportFragment() {
)
}
private fun onCreateStream(room: Room, rowsAdapter: ArrayObjectAdapter) {
val detailsOverview = DetailsOverviewRow(room)
setThumb(room.thumb, detailsOverview)
initializeBackgroundWithImage(room.thumb)
val dashStreams = room.streams.filter { it.slug == "dash-native" }
if (dashStreams.isNotEmpty()) { // && detailsViewModel.autoselectStream ) {
dashStreams.first().urls["dash"]?.url?.let { preparePlayer(it, "") }
}
val actionAdapter = ArrayObjectAdapter()
val playAction = Action(ACTION_PLAY, "Play")
actionAdapter.add(playAction)
detailsOverview.actionsAdapter = actionAdapter
rowsAdapter.add(detailsOverview)
}
override fun onPause() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || activity?.isInPictureInPictureMode == false) {
event?.let { event ->
playerViewModel.setPlaybackProgress(event.guid, playerAdapter.currentPosition)
}
playerViewModel.setPlaybackProgress(event.guid, playerAdapter.currentPosition)
playerAdapter.pause()
}
super.onPause()
@ -372,7 +336,7 @@ class EventDetailsFragment : DetailsSupportFragment() {
private fun buildPlayerGlue(): ChaosMediaPlayerGlue {
playerAdapter = LeanbackPlayerAdapter(context, player, 16)
return ChaosMediaPlayerGlue(requireContext(), playerAdapter) {
event?.guid?.let { detailsViewModel.createBookmark(it) }
detailsViewModel.createBookmark(event.guid)
}
}
@ -446,18 +410,18 @@ class EventDetailsFragment : DetailsSupportFragment() {
Log.d(TAG, "OnActionClicked")
when (action.id) {
ACTION_ADD_WATCHLIST -> {
event?.guid?.let { detailsViewModel.createBookmark(it) }
detailsViewModel.createBookmark(event.guid)
val preferences = requireActivity().getSharedPreferences(getString(R.string.watchlist_preferences_key), Context.MODE_PRIVATE)
if (preferences.getBoolean(getString(R.string.watchlist_dialog_needed), true)) { // new item
showWatchlistInfoDialog(preferences)
}
}
ACTION_REMOVE_WATCHLIST -> {
event?.guid?.let { detailsViewModel.removeBookmark(it) }
detailsViewModel.removeBookmark(event.guid)
}
ACTION_PLAY -> {
if (player.playbackState == Player.STATE_IDLE) {
event?.let { detailsViewModel.play(it) }
detailsViewModel.play(event)
} else {
detailsBackgroundController.switchToVideo()
}

View file

@ -5,8 +5,8 @@ import androidx.fragment.app.FragmentManager
import androidx.leanback.app.GuidedStepSupportFragment
import androidx.leanback.widget.GuidanceStylist.Guidance
import androidx.leanback.widget.GuidedAction
import de.nicidienase.chaosflix.common.ChaosflixUtil
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Recording
import de.nicidienase.chaosflix.common.util.RecordingUtil
import de.nicidienase.chaosflix.leanback.R
class RecordingSelectDialog private constructor() : GuidedStepSupportFragment() {
@ -23,7 +23,7 @@ class RecordingSelectDialog private constructor() : GuidedStepSupportFragment()
recordings.forEach {
val action = GuidedAction.Builder(requireContext())
.id(it.id)
.title(RecordingUtil.getStringForRecording(it))
.title(ChaosflixUtil.getStringForRecording(it))
.build()
actions.add(action)
}
@ -36,7 +36,7 @@ class RecordingSelectDialog private constructor() : GuidedStepSupportFragment()
}
fun show(fragmentManager: FragmentManager) {
add(fragmentManager, this, R.id.details_fragment)
add(fragmentManager, this, R.id.details_fragment_container)
}
companion object {

View file

@ -0,0 +1,303 @@
package de.nicidienase.chaosflix.leanback.detail
import android.app.AlertDialog
import android.content.DialogInterface
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.leanback.app.DetailsSupportFragment
import androidx.leanback.app.DetailsSupportFragmentBackgroundController
import androidx.leanback.widget.Action
import androidx.leanback.widget.ArrayObjectAdapter
import androidx.leanback.widget.ClassPresenterSelector
import androidx.leanback.widget.DetailsOverviewRow
import androidx.leanback.widget.FullWidthDetailsOverviewRowPresenter
import androidx.leanback.widget.FullWidthDetailsOverviewSharedElementHelper
import androidx.leanback.widget.ListRow
import androidx.leanback.widget.ListRowPresenter
import androidx.leanback.widget.OnActionClickedListener
import androidx.lifecycle.ViewModelProvider
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayerFactory
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.ext.leanback.LeanbackPlayerAdapter
import com.google.android.exoplayer2.source.ExtractorMediaSource
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.dash.DashMediaSource
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
import com.google.android.exoplayer2.upstream.HttpDataSource
import com.google.android.exoplayer2.util.Util
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.Room
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.Stream
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.StreamUrl
import de.nicidienase.chaosflix.common.mediadata.network.ApiFactory
import de.nicidienase.chaosflix.common.viewmodel.DetailsViewModel
import de.nicidienase.chaosflix.common.viewmodel.PlayerViewModel
import de.nicidienase.chaosflix.common.viewmodel.ViewModelFactory
import de.nicidienase.chaosflix.leanback.ItemViewClickedListener
import de.nicidienase.chaosflix.leanback.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
class StreamDetailsFragment : DetailsSupportFragment() {
private var selectDialog: AlertDialog? = null
private lateinit var detailsViewModel: DetailsViewModel
private lateinit var playerViewModel: PlayerViewModel
private lateinit var room: Room
private lateinit var rowsAdapter: ArrayObjectAdapter
private val detailsBackgroundController = DetailsSupportFragmentBackgroundController(this)
private val playerDelegate = lazy {
ExoPlayerFactory.newSimpleInstance(
activity,
DefaultTrackSelector(
AdaptiveTrackSelection.Factory()))
}
private val player: SimpleExoPlayer by playerDelegate
private lateinit var playerAdapter: LeanbackPlayerAdapter
private lateinit var playerGlue: ChaosMediaPlayerGlue
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val viewModelFactory = ViewModelFactory.getInstance(requireContext())
detailsViewModel = ViewModelProvider(this, viewModelFactory).get(DetailsViewModel::class.java)
playerViewModel = ViewModelProvider(this, viewModelFactory).get(PlayerViewModel::class.java)
room = arguments?.getParcelable(DetailsActivity.ROOM) ?: error("Missing Argument Room")
val selector = ClassPresenterSelector()
val detailsPresenter = FullWidthDetailsOverviewRowPresenter(
EventDetailsDescriptionPresenter(requireContext()))
val helper = FullWidthDetailsOverviewSharedElementHelper()
helper.setSharedElementEnterTransition(activity,
DetailsActivity.SHARED_ELEMENT_NAME)
detailsPresenter.setListener(helper)
prepareEntranceTransition()
detailsPresenter.onActionClickedListener = DetailActionClickedListener()
selector.addClassPresenter(DetailsOverviewRow::class.java, detailsPresenter)
selector.addClassPresenter(ListRow::class.java, ListRowPresenter())
rowsAdapter = ArrayObjectAdapter(selector)
detailsBackgroundController.enableParallax()
playerGlue = buildPlayerGlue()
playerGlue.title = room.display
detailsBackgroundController.setupVideoPlayback(playerGlue)
onItemViewClickedListener = ItemViewClickedListener(this)
onCreateStream(room, rowsAdapter)
adapter = this.rowsAdapter
startEntranceTransition()
}
private fun showError(errorMessage: String?) {
if (errorMessage != null && errorMessage.isNotBlank()) {
view?.let {
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
}
}
}
private fun selectStreamFromList(room: Room, streams: List<Stream>, resultHandler: (StreamUrl) -> Unit) {
if (selectDialog != null) {
selectDialog?.dismiss()
}
val streamUrls: Map<String, StreamUrl> = streams.flatMap { stream -> stream.urls.map { "${stream.display} ${it.key}" to it.value } }.toMap()
val keys = streamUrls.keys.toTypedArray()
val onClickListener = DialogInterface.OnClickListener { _, which ->
val selectedKey = keys[which]
streamUrls[selectedKey]?.let { resultHandler.invoke(it) } ?: Log.e(TAG, "Selected Stream somehow does not exist")
}
selectDialog = AlertDialog.Builder(requireContext())
.setItems(keys, onClickListener)
.setNegativeButton("Autoselect") { _, _ ->
getAutoselectedStream(room)?.let { resultHandler.invoke(it) }
}
.setPositiveButton("Always select automatically") { _, _ ->
detailsViewModel.autoselectStream = true
getAutoselectedStream(room)?.let { resultHandler.invoke(it) }
}
.create()
selectDialog?.show()
}
private fun onCreateStream(room: Room, rowsAdapter: ArrayObjectAdapter) {
val detailsOverview = DetailsOverviewRow(room)
setThumb(room.thumb, detailsOverview)
initializeBackgroundWithImage(room.thumb)
val streamUrl = getAutoselectedStream(room)
if (detailsViewModel.autoselectStream && streamUrl != null) {
detailsBackgroundController.switchToVideo()
preparePlayer(streamUrl.url, "")
} else {
selectStreamFromList(room, room.streams) {
detailsBackgroundController.switchToVideo()
preparePlayer(it.url)
}
}
val actionAdapter = ArrayObjectAdapter()
val playAction = Action(ACTION_PLAY, "Play")
actionAdapter.add(playAction)
detailsOverview.actionsAdapter = actionAdapter
rowsAdapter.add(detailsOverview)
}
private fun getAutoselectedStream(room: Room): StreamUrl? {
val dashStreams = room.streams.filter { it.slug == "dash-native" }
return dashStreams.first().urls["dash"]
}
override fun onPause() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || activity?.isInPictureInPictureMode == false) {
playerAdapter.pause()
}
super.onPause()
}
override fun onStop() {
super.onStop()
if (playerDelegate.isInitialized()) {
player.release()
}
}
private fun setThumb(thumbUrl: String, detailsOverview: DetailsOverviewRow) {
Glide.with(requireContext())
.asBitmap()
.load(thumbUrl)
.into(object : SimpleTarget<Bitmap>(DETAIL_THUMB_WIDTH, DETAIL_THUMB_HEIGHT) {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
detailsOverview.setImageBitmap(requireContext(), resource)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
detailsOverview.imageDrawable = ContextCompat.getDrawable(requireContext(), DEFAULT_DRAWABLE)
}
})
}
private fun initializeBackgroundWithImage(url: String) {
detailsBackgroundController.enableParallax()
val options = RequestOptions()
.fallback(R.drawable.default_background)
Glide.with(requireContext())
.asBitmap()
.load(url)
.apply(options)
.into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
detailsBackgroundController.coverBitmap = resource
}
})
}
private fun buildPlayerGlue(): ChaosMediaPlayerGlue {
playerAdapter = LeanbackPlayerAdapter(context, player, 16)
return ChaosMediaPlayerGlue(requireContext(), playerAdapter)
}
private fun preparePlayer(url: String, overrideExtension: String = "") {
player.prepare(buildMediaSource(Uri.parse(url), overrideExtension))
}
private fun buildMediaSource(uri: Uri, overrideExtension: String): MediaSource {
val mediaDataSourceFactory = buildDataSourceFactory()
val type = if (TextUtils.isEmpty(overrideExtension)) {
Util.inferContentType(uri)
} else
Util.inferContentType(".$overrideExtension")
when (type) {
C.TYPE_DASH -> return DashMediaSource.Factory(
DefaultDashChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory())
.createMediaSource(uri)
C.TYPE_HLS -> return HlsMediaSource.Factory(buildDataSourceFactory())
.createMediaSource(uri)
C.TYPE_SS, C.TYPE_OTHER -> return ExtractorMediaSource.Factory(mediaDataSourceFactory)
.createMediaSource(uri)
else -> {
throw IllegalStateException("Unsupported type: $type")
}
}
}
private fun buildDataSourceFactory(useBandwidthMeter: Boolean = true): DataSource.Factory {
return buildDataSourceFactory(if (useBandwidthMeter) BANDWIDTH_METER else null)
}
private fun buildDataSourceFactory(bandwidthMeter: DefaultBandwidthMeter?): DataSource.Factory {
return DefaultDataSourceFactory(requireContext(), bandwidthMeter,
buildHttpDataSourceFactory(bandwidthMeter))
}
private fun buildHttpDataSourceFactory(bandwidthMeter: DefaultBandwidthMeter?): HttpDataSource.Factory {
return DefaultHttpDataSourceFactory(
ApiFactory.buildUserAgent(),
bandwidthMeter,
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
true /* allowCrossProtocolRedirects */)
}
companion object {
private val BANDWIDTH_METER = DefaultBandwidthMeter()
@JvmStatic
val TAG = StreamDetailsFragment::class.java.simpleName
@JvmStatic
private val DETAIL_THUMB_WIDTH = 254
@JvmStatic
private val DETAIL_THUMB_HEIGHT = 143
@JvmStatic
val DEFAULT_DRAWABLE = R.drawable.default_background
@JvmStatic
private val ACTION_PLAY: Long = 0L
}
private inner class DetailActionClickedListener : OnActionClickedListener {
override fun onActionClicked(action: Action) {
Log.d(TAG, "OnActionClicked")
when (action.id) {
ACTION_PLAY -> {
detailsBackgroundController.switchToVideo()
}
}
}
}
}

View file

@ -39,18 +39,6 @@ class ChaosflixSettingsFragment : LeanbackSettingsFragment() {
} else {
setPreferencesFromResource(prefResId, root)
}
// Disable preferences not relevant for leanback
// listOf("allow_metered_networks",
// "auto_external_player",
// "download_folder",
// "delete_data",
// "export_favorites",
// "import_favorites").forEach {
// preferenceManager.findPreference(it).apply {
// isVisible = false
// isEnabled = false
// }
// }
}
}
}

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<fragment android:id="@+id/details_fragment"
android:name="de.nicidienase.chaosflix.leanback.detail.EventDetailsFragment"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".leanback.activities.DetailsActivity"
tools:deviceIds="tv"/>
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/details_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".detail.DetailsActivity"
tools:deviceIds="tv"/>

View file

@ -14,17 +14,17 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.google.android.material.snackbar.Snackbar
import de.nicidienase.chaosflix.common.ChaosflixUtil
import de.nicidienase.chaosflix.common.OfflineItemManager
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Event
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Recording
import de.nicidienase.chaosflix.common.util.RecordingUtil
import de.nicidienase.chaosflix.common.viewmodel.DetailsViewModel
import de.nicidienase.chaosflix.common.viewmodel.ViewModelFactory
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
import de.nicidienase.chaosflix.touch.R
import de.nicidienase.chaosflix.touch.browse.cast.CastService
import de.nicidienase.chaosflix.touch.playback.PlayerActivity
import kotlinx.android.synthetic.main.activity_eventdetails.*
import kotlinx.android.synthetic.main.activity_eventdetails.fragment_container
class EventDetailsActivity : AppCompatActivity(),
EventDetailsFragment.OnEventDetailsFragmentInteractionListener,
@ -140,7 +140,7 @@ class EventDetailsActivity : AppCompatActivity(),
}
private fun selectRecording(event: Event, recordings: List<Recording>, action: (Event, Recording) -> Unit) {
val items: List<String> = recordings.map { RecordingUtil.getStringForRecording(it) }
val items: List<String> = recordings.map { ChaosflixUtil.getStringForRecording(it) }
selectRecordingFromList(items) { i ->
action.invoke(event, recordings[i])
}