From 3faaeae487917cacf9fcc320c50e076fea9720fb Mon Sep 17 00:00:00 2001 From: Stefan Medack Date: Sat, 30 Dec 2017 20:33:38 +0100 Subject: [PATCH 01/16] renames package and classes dedicated to playback for streaming --- app/src/main/AndroidManifest.xml | 2 +- .../ccctv/ui/main/LiveStreamingFragment.kt | 4 +-- .../StreamingMediaPlayerGlue.kt} | 4 +-- .../StreamingPlayerActivity.kt} | 10 +++--- .../StreamingPlayerAdapter.kt} | 36 +++++++++---------- .../StreamingPlayerFragment.kt} | 10 +++--- 6 files changed, 33 insertions(+), 33 deletions(-) rename app/src/main/java/de/stefanmedack/ccctv/ui/{playback/VideoMediaPlayerGlue.kt => streaming/StreamingMediaPlayerGlue.kt} (66%) rename app/src/main/java/de/stefanmedack/ccctv/ui/{playback/ExoPlayerActivity.kt => streaming/StreamingPlayerActivity.kt} (85%) rename app/src/main/java/de/stefanmedack/ccctv/ui/{playback/ExoPlayerAdapter.kt => streaming/StreamingPlayerAdapter.kt} (87%) rename app/src/main/java/de/stefanmedack/ccctv/ui/{playback/ExoPlayerFragment.kt => streaming/StreamingPlayerFragment.kt} (88%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5981dd4..389367a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -92,7 +92,7 @@ /> if (item is Stream) { - ExoPlayerActivity.start(activity, item) + StreamingPlayerActivity.start(activity, item) } } } diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/playback/VideoMediaPlayerGlue.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingMediaPlayerGlue.kt similarity index 66% rename from app/src/main/java/de/stefanmedack/ccctv/ui/playback/VideoMediaPlayerGlue.kt rename to app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingMediaPlayerGlue.kt index 6b973e3..061aa4d 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/playback/VideoMediaPlayerGlue.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingMediaPlayerGlue.kt @@ -1,4 +1,4 @@ -package de.stefanmedack.ccctv.ui.playback +package de.stefanmedack.ccctv.ui.streaming import android.app.Activity import android.support.v17.leanback.media.PlaybackTransportControlGlue @@ -10,4 +10,4 @@ import android.support.v17.leanback.media.PlayerAdapter * PlayerGlue for video playback * @param */ -class VideoMediaPlayerGlue(context: Activity, impl: T) : PlaybackTransportControlGlue(context, impl) \ No newline at end of file +class StreamingMediaPlayerGlue(context: Activity, impl: T) : PlaybackTransportControlGlue(context, impl) \ No newline at end of file diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/playback/ExoPlayerActivity.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerActivity.kt similarity index 85% rename from app/src/main/java/de/stefanmedack/ccctv/ui/playback/ExoPlayerActivity.kt rename to app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerActivity.kt index d9ccb17..b06c1a3 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/playback/ExoPlayerActivity.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerActivity.kt @@ -1,4 +1,4 @@ -package de.stefanmedack.ccctv.ui.playback +package de.stefanmedack.ccctv.ui.streaming import android.content.Intent import android.os.Bundle @@ -10,7 +10,7 @@ import de.stefanmedack.ccctv.util.addFragmentInTransaction import info.metadude.java.library.brockman.models.Stream import info.metadude.java.library.brockman.models.Url.TYPE -class ExoPlayerActivity : FragmentActivity() { +class StreamingPlayerActivity : FragmentActivity() { public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -19,12 +19,12 @@ class ExoPlayerActivity : FragmentActivity() { // prevent stand-by while playing videos window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - val fragment = ExoPlayerFragment().apply { + val fragment = StreamingPlayerFragment().apply { arguments = Bundle(1).also { it.putString(STREAM_URL, intent.getStringExtra(STREAM_URL)) } } - addFragmentInTransaction(fragment, R.id.videoFragment, ExoPlayerFragment.TAG) + addFragmentInTransaction(fragment, R.id.videoFragment, StreamingPlayerFragment.TAG) } override fun onVisibleBehindCanceled() { @@ -45,7 +45,7 @@ class ExoPlayerActivity : FragmentActivity() { companion object { fun start(activity: FragmentActivity, item: Stream) { - val intent = Intent(activity, ExoPlayerActivity::class.java) + val intent = Intent(activity, StreamingPlayerActivity::class.java) intent.putExtra(STREAM_URL, item.urls.find { it.type == TYPE.WEBM }?.url ?: item.urls[0].url) activity.startActivity(intent) } diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/playback/ExoPlayerAdapter.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerAdapter.kt similarity index 87% rename from app/src/main/java/de/stefanmedack/ccctv/ui/playback/ExoPlayerAdapter.kt rename to app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerAdapter.kt index eab6262..503f91f 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/playback/ExoPlayerAdapter.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerAdapter.kt @@ -1,4 +1,4 @@ -package de.stefanmedack.ccctv.ui.playback +package de.stefanmedack.ccctv.ui.streaming import android.content.Context import android.net.Uri @@ -24,15 +24,15 @@ import de.stefanmedack.ccctv.R /** * This implementation extends the [PlayerAdapter] with a [SimpleExoPlayer]. */ -class ExoPlayerAdapter(context: Context) : PlayerAdapter(), Player.EventListener { +class StreamingPlayerAdapter(context: Context) : PlayerAdapter(), Player.EventListener { var context: Context internal set internal val mPlayer: SimpleExoPlayer internal var mSurfaceHolderGlueHost: SurfaceHolderGlueHost? = null internal val mRunnable: Runnable = object : Runnable { override fun run() { - callback.onCurrentPositionChanged(this@ExoPlayerAdapter) - callback.onBufferedPositionChanged(this@ExoPlayerAdapter) + callback.onCurrentPositionChanged(this@StreamingPlayerAdapter) + callback.onBufferedPositionChanged(this@StreamingPlayerAdapter) mHandler.postDelayed(this, updatePeriod.toLong()) } } @@ -74,7 +74,7 @@ class ExoPlayerAdapter(context: Context) : PlayerAdapter(), Player.EventListener mInitialized = false notifyBufferingStartEnd() if (mHasDisplay) { - callback.onPreparedStateChanged(this@ExoPlayerAdapter) + callback.onPreparedStateChanged(this@StreamingPlayerAdapter) } } } @@ -84,7 +84,7 @@ class ExoPlayerAdapter(context: Context) : PlayerAdapter(), Player.EventListener * according to the state of buffering. */ internal fun notifyBufferingStartEnd() { - callback.onBufferingStateChanged(this@ExoPlayerAdapter, + callback.onBufferingStateChanged(this@StreamingPlayerAdapter, mBufferingStart || !mInitialized) } @@ -119,11 +119,11 @@ class ExoPlayerAdapter(context: Context) : PlayerAdapter(), Player.EventListener mPlayer.setVideoSurfaceHolder(surfaceHolder) if (mHasDisplay) { if (mInitialized) { - callback.onPreparedStateChanged(this@ExoPlayerAdapter) + callback.onPreparedStateChanged(this@StreamingPlayerAdapter) } } else { if (mInitialized) { - callback.onPreparedStateChanged(this@ExoPlayerAdapter) + callback.onPreparedStateChanged(this@StreamingPlayerAdapter) } } } @@ -159,14 +159,14 @@ class ExoPlayerAdapter(context: Context) : PlayerAdapter(), Player.EventListener } mPlayer.playWhenReady = true - callback.onPlayStateChanged(this@ExoPlayerAdapter) - callback.onCurrentPositionChanged(this@ExoPlayerAdapter) + callback.onPlayStateChanged(this@StreamingPlayerAdapter) + callback.onCurrentPositionChanged(this@StreamingPlayerAdapter) } override fun pause() { if (isPlaying) { mPlayer.playWhenReady = false - callback.onPlayStateChanged(this@ExoPlayerAdapter) + callback.onPlayStateChanged(this@StreamingPlayerAdapter) } } @@ -199,7 +199,7 @@ class ExoPlayerAdapter(context: Context) : PlayerAdapter(), Player.EventListener * @return MediaSource for the player */ fun onCreateMediaSource(uri: Uri): MediaSource { - val userAgent = Util.getUserAgent(context, "ExoPlayerAdapter") + val userAgent = Util.getUserAgent(context, "StreamingPlayerAdapter") return ExtractorMediaSource(uri, DefaultHttpDataSourceFactory(userAgent, null, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, true), @@ -231,13 +231,13 @@ class ExoPlayerAdapter(context: Context) : PlayerAdapter(), Player.EventListener mPlayer.setVideoListener(object : SimpleExoPlayer.VideoListener { override fun onVideoSizeChanged(width: Int, height: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) { - callback.onVideoSizeChanged(this@ExoPlayerAdapter, width, height) + callback.onVideoSizeChanged(this@StreamingPlayerAdapter, width, height) } override fun onRenderedFirstFrame() {} }) notifyBufferingStartEnd() - callback.onPlayStateChanged(this@ExoPlayerAdapter) + callback.onPlayStateChanged(this@StreamingPlayerAdapter) } /** @@ -271,19 +271,19 @@ class ExoPlayerAdapter(context: Context) : PlayerAdapter(), Player.EventListener if (playbackState == Player.STATE_READY && !mInitialized) { mInitialized = true if (mSurfaceHolderGlueHost == null || mHasDisplay) { - callback.onPreparedStateChanged(this@ExoPlayerAdapter) + callback.onPreparedStateChanged(this@StreamingPlayerAdapter) } } else if (playbackState == Player.STATE_BUFFERING) { mBufferingStart = true } else if (playbackState == Player.STATE_ENDED) { - callback.onPlayStateChanged(this@ExoPlayerAdapter) - callback.onPlayCompleted(this@ExoPlayerAdapter) + callback.onPlayStateChanged(this@StreamingPlayerAdapter) + callback.onPlayCompleted(this@StreamingPlayerAdapter) } notifyBufferingStartEnd() } override fun onPlayerError(error: ExoPlaybackException) { - callback.onError(this@ExoPlayerAdapter, error.type, + callback.onError(this@StreamingPlayerAdapter, error.type, context.getString(R.string.lb_media_player_error, error.type, error.rendererIndex)) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/playback/ExoPlayerFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerFragment.kt similarity index 88% rename from app/src/main/java/de/stefanmedack/ccctv/ui/playback/ExoPlayerFragment.kt rename to app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerFragment.kt index d917a83..4e36464 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/playback/ExoPlayerFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerFragment.kt @@ -1,4 +1,4 @@ -package de.stefanmedack.ccctv.ui.playback +package de.stefanmedack.ccctv.ui.streaming import android.content.Context import android.media.AudioManager @@ -12,9 +12,9 @@ import android.support.v17.leanback.media.PlaybackGlue import android.util.Log import de.stefanmedack.ccctv.util.STREAM_URL -class ExoPlayerFragment : VideoSupportFragment() { +class StreamingPlayerFragment : VideoSupportFragment() { - private lateinit var mediaPlayerGlue: VideoMediaPlayerGlue + private lateinit var mediaPlayerGlue: StreamingMediaPlayerGlue private val glueHost = VideoSupportFragmentGlueHost(this) private val onAudioFocusChangeListener: AudioManager.OnAudioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { } @@ -22,9 +22,9 @@ class ExoPlayerFragment : VideoSupportFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val playerAdapter = ExoPlayerAdapter(activity) + val playerAdapter = StreamingPlayerAdapter(activity) playerAdapter.audioStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE - mediaPlayerGlue = VideoMediaPlayerGlue(activity, playerAdapter) + mediaPlayerGlue = StreamingMediaPlayerGlue(activity, playerAdapter) mediaPlayerGlue.host = glueHost val audioManager = activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager if (audioManager.requestAudioFocus(onAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) From ea7861062544eeefd44dc00ee90e62ee09e763c8 Mon Sep 17 00:00:00 2001 From: Stefan Medack Date: Sat, 30 Dec 2017 22:02:08 +0100 Subject: [PATCH 02/16] pause playback when opening HUD on Amazon devices --- .../de/stefanmedack/ccctv/ui/detail/DetailActivity.kt | 6 ------ .../de/stefanmedack/ccctv/ui/detail/DetailFragment.kt | 8 ++++++++ .../ccctv/ui/detail/playback/ExoPlayerAdapter.kt | 9 +++++---- .../ccctv/ui/streaming/StreamingPlayerActivity.kt | 6 ------ 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailActivity.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailActivity.kt index 6710b12..52cd7a3 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailActivity.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailActivity.kt @@ -42,12 +42,6 @@ class DetailActivity : BaseInjectableActivity() { return (fragment?.onKeyDown(keyCode) == true) || super.onKeyDown(keyCode, event) } - // TODO workaround for amazon - move to new implementation - override fun onVisibleBehindCanceled() { - mediaController?.transportControls?.pause() - super.onVisibleBehindCanceled() - } - companion object { fun start(activity: Activity, event: Event, sharedImage: ImageView? = null) { val intent = Intent(activity, DetailActivity::class.java) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailFragment.kt index c100c62..b1b17dd 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailFragment.kt @@ -2,6 +2,7 @@ package de.stefanmedack.ccctv.ui.detail import android.arch.lifecycle.ViewModelProvider import android.arch.lifecycle.ViewModelProviders +import android.os.Build import android.os.Bundle import android.support.v17.leanback.app.DetailsSupportFragment import android.support.v17.leanback.app.DetailsSupportFragmentBackgroundController @@ -53,6 +54,13 @@ class DetailFragment : DetailsSupportFragment() { bindViewModel() } + override fun onPause() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || !activity.isInPictureInPictureMode) { + detailsBackground.playbackGlue?.pause() + } + super.onPause() + } + override fun onDestroy() { disposables.clear() super.onDestroy() diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/ExoPlayerAdapter.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/ExoPlayerAdapter.kt index 61ad078..7f3bf5b 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/ExoPlayerAdapter.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/ExoPlayerAdapter.kt @@ -315,10 +315,11 @@ class ExoPlayerAdapter(private val context: Context) : PlayerAdapter(), Player.E } override fun onPlayerError(error: ExoPlaybackException) { - callback.onError(this@ExoPlayerAdapter, error.type, - context.getString(R.string.lb_media_player_error, - error.type, - error.rendererIndex)) + callback.onError( + this@ExoPlayerAdapter, + error.type, + context.getString(R.string.lb_media_player_error, error.type, error.rendererIndex) + ) } override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {} diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerActivity.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerActivity.kt index b06c1a3..08c5061 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerActivity.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerActivity.kt @@ -27,12 +27,6 @@ class StreamingPlayerActivity : FragmentActivity() { addFragmentInTransaction(fragment, R.id.videoFragment, StreamingPlayerFragment.TAG) } - override fun onVisibleBehindCanceled() { - mediaController?.transportControls?.pause() - super.onVisibleBehindCanceled() - } - - // TODO workaround for amazon - move to new implementation override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) // This part is necessary to ensure that getIntent returns the latest intent when From 28c65b6129c5f8c1d9955f407f1dc63ad53e946d Mon Sep 17 00:00:00 2001 From: Stefan Medack Date: Sat, 30 Dec 2017 22:29:44 +0100 Subject: [PATCH 03/16] updates Support Lib --- app/build.gradle | 9 ++-- .../ccctv/ui/about/AboutFragment.kt | 16 ++++--- .../ccctv/ui/detail/DetailFragment.kt | 45 +++++++++++-------- .../ui/main/GroupedConferencesFragment.kt | 8 ++-- .../ccctv/ui/main/LiveStreamingFragment.kt | 8 ++-- .../ccctv/ui/main/MainFragment.kt | 19 +++++--- .../ccctv/ui/search/SearchFragment.kt | 15 +++---- .../ui/streaming/StreamingPlayerFragment.kt | 25 ++++++----- 8 files changed, 84 insertions(+), 61 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5888d8d..446b998 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,7 +23,7 @@ ext { rxBindingVersion = "2.0.0" rxJavaVersion = "2.1.4" rxKotlinVersion = "2.1.0" - supportLibVersion = "26.1.0" + supportLibVersion = "27.0.2" timberVersion = "4.5.1" // Test dependencies @@ -35,12 +35,12 @@ ext { android { - compileSdkVersion 26 - buildToolsVersion '26.0.2' + compileSdkVersion 27 + buildToolsVersion '27.0.1' defaultConfig { applicationId "de.stefanmedack.ccctv" minSdkVersion 21 - targetSdkVersion 26 + targetSdkVersion 27 versionCode 9 versionName "2.0.0" resConfigs "en", "de" @@ -102,6 +102,7 @@ dependencies { // Support Libs implementation "com.android.support:appcompat-v7:$supportLibVersion" + implementation "com.android.support:support-v4:$supportLibVersion" implementation "com.android.support:leanback-v17:$supportLibVersion" // Architecture Components - View Model (+ Lifecycles, LiveData) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/about/AboutFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/about/AboutFragment.kt index 9aa5c77..00297e5 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/about/AboutFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/about/AboutFragment.kt @@ -1,5 +1,6 @@ package de.stefanmedack.ccctv.ui.about +import android.content.Context import android.content.Intent import android.graphics.BitmapFactory import android.os.Bundle @@ -8,6 +9,7 @@ import android.support.v17.leanback.app.DetailsSupportFragment import android.support.v17.leanback.app.DetailsSupportFragmentBackgroundController import android.support.v17.leanback.widget.* import android.support.v4.content.ContextCompat +import android.view.View import android.widget.Toast import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import de.stefanmedack.ccctv.R @@ -27,14 +29,14 @@ class AboutFragment : DetailsSupportFragment(), BrowseSupportFragment.MainFragme return mMainFragmentAdapter } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - setupUi() + setupUi(view.context) setupEventListeners() } - private fun setupUi() { + private fun setupUi(context: Context) { DetailsSupportFragmentBackgroundController(this).apply { enableParallax() coverBitmap = BitmapFactory.decodeResource(resources, @@ -42,8 +44,8 @@ class AboutFragment : DetailsSupportFragment(), BrowseSupportFragment.MainFragme } val detailOverviewRowPresenter = FullWidthDetailsOverviewRowPresenter(AboutDescriptionPresenter()) - detailOverviewRowPresenter.actionsBackgroundColor = ContextCompat.getColor(activity, R.color.teal_900) - detailOverviewRowPresenter.backgroundColor = ContextCompat.getColor(activity, R.color.teal_900) + detailOverviewRowPresenter.actionsBackgroundColor = ContextCompat.getColor(context, R.color.teal_900) + detailOverviewRowPresenter.backgroundColor = ContextCompat.getColor(context, R.color.teal_900) val detailsOverview = DetailsOverviewRow( AboutDescription( @@ -51,7 +53,7 @@ class AboutFragment : DetailsSupportFragment(), BrowseSupportFragment.MainFragme description = getString(R.string.about_description) ) ) - detailsOverview.imageDrawable = ContextCompat.getDrawable(activity, R.drawable.store_qr) + detailsOverview.imageDrawable = ContextCompat.getDrawable(context, R.drawable.store_qr) adapter = ArrayObjectAdapter( // Setup PresenterSelector to distinguish between the different rows. diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailFragment.kt index b1b17dd..99e93e8 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailFragment.kt @@ -2,6 +2,7 @@ package de.stefanmedack.ccctv.ui.detail import android.arch.lifecycle.ViewModelProvider import android.arch.lifecycle.ViewModelProviders +import android.content.Context import android.os.Build import android.os.Bundle import android.support.v17.leanback.app.DetailsSupportFragment @@ -36,7 +37,7 @@ class DetailFragment : DetailsSupportFragment() { private val viewModel: DetailViewModel by lazy { ViewModelProviders.of(this, viewModelFactory).get(DetailViewModel::class.java).apply { - init(arguments.getInt(EVENT_ID)) + init(arguments?.getInt(EVENT_ID) ?: -1) } } @@ -48,14 +49,18 @@ class DetailFragment : DetailsSupportFragment() { override fun onCreate(savedInstanceState: Bundle?) { AndroidSupportInjection.inject(this) super.onCreate(savedInstanceState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - setupUi() + setupUi(view.context) setupEventListeners() bindViewModel() } override fun onPause() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || !activity.isInPictureInPictureMode) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || activity?.isInPictureInPictureMode == false) { detailsBackground.playbackGlue?.pause() } super.onPause() @@ -66,12 +71,12 @@ class DetailFragment : DetailsSupportFragment() { super.onDestroy() } - private fun setupUi() { + private fun setupUi(context: Context) { detailsBackground = DetailsSupportFragmentBackgroundController(this) // detail overview row - presents the detail, description and actions val detailOverviewRowPresenter = FullWidthDetailsOverviewRowPresenter(DetailDescriptionPresenter()) - detailOverviewRowPresenter.actionsBackgroundColor = ContextCompat.getColor(activity, R.color.amber_800) + detailOverviewRowPresenter.actionsBackgroundColor = ContextCompat.getColor(context, R.color.amber_800) // init Shared Element Transition detailOverviewRowPresenter.setListener(FullWidthDetailsOverviewSharedElementHelper().apply { @@ -81,7 +86,7 @@ class DetailFragment : DetailsSupportFragment() { // Setup action and detail row. detailsOverview = DetailsOverviewRow(Any()) - showPoster(detailsOverview) + showPoster(context, detailsOverview) detailsOverview.actionsAdapter = ArrayObjectAdapter().apply { add(Action(DETAIL_ACTION_PLAY, getString(R.string.action_watch))) @@ -102,11 +107,11 @@ class DetailFragment : DetailsSupportFragment() { } } - private fun showPoster(detailsOverview: DetailsOverviewRow) { - detailsOverview.imageDrawable = ContextCompat.getDrawable(activity, R.drawable.voctocat) + private fun showPoster(context: Context, detailsOverview: DetailsOverviewRow) { + detailsOverview.imageDrawable = ContextCompat.getDrawable(context, R.drawable.voctocat) Glide.with(activity) - .load(arguments.getString(EVENT_PICTURE)) + .load(arguments?.getString(EVENT_PICTURE)) .centerCrop() .error(R.drawable.voctocat) .into>(object : SimpleTarget( @@ -131,15 +136,17 @@ class DetailFragment : DetailsSupportFragment() { private fun render(result: DetailUiModel) { detailsOverview.item = result.event - val playerAdapter = ExoPlayerAdapter(activity) - val mediaPlayerGlue = VideoMediaPlayerGlue(activity, playerAdapter) - mediaPlayerGlue.isSeekEnabled = true - mediaPlayerGlue.title = result.event.title - mediaPlayerGlue.subtitle = result.event.subtitle - mediaPlayerGlue.playerAdapter.bindRecordings(viewModel.eventWithRecordings) + activity?.let { activityContext -> + val playerAdapter = ExoPlayerAdapter(activityContext) + val mediaPlayerGlue = VideoMediaPlayerGlue(activityContext, playerAdapter) + mediaPlayerGlue.isSeekEnabled = true + mediaPlayerGlue.title = result.event.title + mediaPlayerGlue.subtitle = result.event.subtitle + mediaPlayerGlue.playerAdapter.bindRecordings(viewModel.eventWithRecordings) - detailsBackground.enableParallax() - detailsBackground.setupVideoPlayback(mediaPlayerGlue) + detailsBackground.enableParallax() + detailsBackground.setupVideoPlayback(mediaPlayerGlue) + } (adapter as ArrayObjectAdapter).apply { // add speaker @@ -177,7 +184,9 @@ class DetailFragment : DetailsSupportFragment() { Toast.makeText(activity, R.string.implement_me_toast, Toast.LENGTH_LONG).show() } is Event -> { - DetailActivity.start(activity, item, (itemViewHolder.view as ImageCardView).mainImageView) + activity?.let { + DetailActivity.start(it, item, (itemViewHolder.view as ImageCardView).mainImageView) + } } } } diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/main/GroupedConferencesFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/main/GroupedConferencesFragment.kt index aeb9dea..61e4ecc 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/main/GroupedConferencesFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/main/GroupedConferencesFragment.kt @@ -25,8 +25,8 @@ class GroupedConferencesFragment : RowsSupportFragment() { lateinit var viewModelFactory: ViewModelProvider.Factory private val viewModel: GroupedConferencesViewModel by lazy { - ViewModelProviders.of(activity, viewModelFactory).get(GroupedConferencesViewModel::class.java).apply { - init(arguments.getString(CONFERENCE_GROUP, "")) + ViewModelProviders.of(this, viewModelFactory).get(GroupedConferencesViewModel::class.java).apply { + init(arguments?.getString(CONFERENCE_GROUP, "") ?: "") } } @@ -49,7 +49,9 @@ class GroupedConferencesFragment : RowsSupportFragment() { adapter = ArrayObjectAdapter(ListRowPresenter()) onItemViewClickedListener = OnItemViewClickedListener { itemViewHolder, item, _, _ -> if (item is Event) { - DetailActivity.start(activity, item, (itemViewHolder.view as ImageCardView).mainImageView) + activity?.let { + DetailActivity.start(it, item, (itemViewHolder.view as ImageCardView).mainImageView) + } } } } diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/main/LiveStreamingFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/main/LiveStreamingFragment.kt index d359e02..b870d19 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/main/LiveStreamingFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/main/LiveStreamingFragment.kt @@ -23,8 +23,8 @@ class LiveStreamingFragment : RowsSupportFragment() { lateinit var viewModelFactory: ViewModelProvider.Factory private val viewModel: LiveStreamingViewModel by lazy { - ViewModelProviders.of(activity, viewModelFactory).get(LiveStreamingViewModel::class.java).apply { - init(arguments.getString(STREAM_ID, "")) + ViewModelProviders.of(this, viewModelFactory).get(LiveStreamingViewModel::class.java).apply { + init(arguments?.getString(STREAM_ID, "") ?: "") } } @@ -47,7 +47,9 @@ class LiveStreamingFragment : RowsSupportFragment() { adapter = ArrayObjectAdapter(ListRowPresenter()) onItemViewClickedListener = OnItemViewClickedListener { _, item, _, _ -> if (item is Stream) { - StreamingPlayerActivity.start(activity, item) + activity?.let { + StreamingPlayerActivity.start(it, item) + } } } } diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainFragment.kt index b77660e..1609e9a 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainFragment.kt @@ -2,6 +2,7 @@ package de.stefanmedack.ccctv.ui.main import android.arch.lifecycle.ViewModelProvider import android.arch.lifecycle.ViewModelProviders +import android.content.Context import android.content.Intent import android.os.Bundle import android.support.v17.leanback.app.BrowseFragment @@ -10,6 +11,7 @@ import android.support.v17.leanback.widget.* import android.support.v4.app.Fragment import android.support.v4.content.ContextCompat import android.view.KeyEvent +import android.view.View import android.widget.Toast import dagger.android.support.AndroidSupportInjection import de.stefanmedack.ccctv.R @@ -27,7 +29,7 @@ class MainFragment : BrowseSupportFragment() { lateinit var viewModelFactory: ViewModelProvider.Factory private val viewModel: MainViewModel by lazy { - ViewModelProviders.of(activity, viewModelFactory).get(MainViewModel::class.java) + ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java) } private val disposables = CompositeDisposable() @@ -36,7 +38,12 @@ class MainFragment : BrowseSupportFragment() { AndroidSupportInjection.inject(this) super.onCreate(savedInstanceState) - setupUi() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi(view.context) bindViewModel() } @@ -45,16 +52,14 @@ class MainFragment : BrowseSupportFragment() { super.onDestroy() } - private fun setupUi() { + private fun setupUi(context: Context) { prepareEntranceTransition() headersState = BrowseFragment.HEADERS_ENABLED isHeadersTransitionOnBackEnabled = true - badgeDrawable = ContextCompat.getDrawable(activity, R.drawable.voctocat) + badgeDrawable = ContextCompat.getDrawable(context, R.drawable.voctocat) - setOnSearchClickedListener { - activity.startActivity(Intent(activity, SearchActivity::class.java)) - } + setOnSearchClickedListener { activity?.startActivity(Intent(activity, SearchActivity::class.java)) } mainFragmentRegistry.registerFragment(PageRow::class.java, PageRowFragmentFactory()) } diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/search/SearchFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/search/SearchFragment.kt index ecd5dad..e926f11 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/search/SearchFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/search/SearchFragment.kt @@ -33,14 +33,14 @@ class SearchFragment : SearchSupportFragment() { lateinit var viewModelFactory: ViewModelProvider.Factory private val viewModel: SearchViewModel by lazy { - ViewModelProviders.of(activity, viewModelFactory).get(SearchViewModel::class.java) + ViewModelProviders.of(this, viewModelFactory).get(SearchViewModel::class.java) } private val disposables = CompositeDisposable() private val rowsAdapter: ArrayObjectAdapter = ArrayObjectAdapter(ListRowPresenter()) - override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { AndroidSupportInjection.inject(this) super.onViewCreated(view, savedInstanceState) @@ -67,13 +67,12 @@ class SearchFragment : SearchSupportFragment() { override fun onQueryTextSubmit(query: String): Boolean = true }) - setOnItemViewClickedListener { _, item, _, _ -> - if (item is Event) DetailActivity.start(activity, item) - } + setOnItemViewClickedListener { _, item, _, _ -> if (item is Event && activity != null) DetailActivity.start(activity!!, item) } - if (!activity.hasPermission(Manifest.permission.RECORD_AUDIO)) { - // SpeechRecognitionCallback is not required and if not provided recognition will be handled using internal speech - // recognizer, in which case you must have RECORD_AUDIO permission + // TODO solve deprecation + if (activity?.hasPermission(Manifest.permission.RECORD_AUDIO) == false) { + // SpeechRecognitionCallback is not required and if not provided recognition will be handled using internal speech recognizer, + // in which case you must have RECORD_AUDIO permission setSpeechRecognitionCallback { try { if (activity != null) { diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerFragment.kt index 4e36464..f6747ce 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/streaming/StreamingPlayerFragment.kt @@ -10,6 +10,7 @@ import android.support.v17.leanback.app.VideoSupportFragment import android.support.v17.leanback.app.VideoSupportFragmentGlueHost import android.support.v17.leanback.media.PlaybackGlue import android.util.Log +import android.view.View import de.stefanmedack.ccctv.util.STREAM_URL class StreamingPlayerFragment : VideoSupportFragment() { @@ -19,20 +20,22 @@ class StreamingPlayerFragment : VideoSupportFragment() { private val glueHost = VideoSupportFragmentGlueHost(this) private val onAudioFocusChangeListener: AudioManager.OnAudioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - val playerAdapter = StreamingPlayerAdapter(activity) + val playerAdapter = StreamingPlayerAdapter(view.context) playerAdapter.audioStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE - mediaPlayerGlue = StreamingMediaPlayerGlue(activity, playerAdapter) - mediaPlayerGlue.host = glueHost - val audioManager = activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager - if (audioManager.requestAudioFocus(onAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) - != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - Log.w(TAG, "video player cannot obtain audio focus!") + activity?.let { activityContext -> + mediaPlayerGlue = StreamingMediaPlayerGlue(activityContext, playerAdapter) + mediaPlayerGlue.host = glueHost + val audioManager = activityContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager + if (audioManager.requestAudioFocus(onAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) + != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + Log.w(TAG, "video player cannot obtain audio focus!") + } } - val streamUrl = arguments.getString(STREAM_URL) + val streamUrl = arguments?.getString(STREAM_URL) if (streamUrl != null) { playVideo(streamUrl) } else { @@ -41,7 +44,7 @@ class StreamingPlayerFragment : VideoSupportFragment() { } override fun onPause() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || !activity.isInPictureInPictureMode) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || activity?.isInPictureInPictureMode == false) { mediaPlayerGlue.pause() } super.onPause() From cd671edfbc09831483a579e7301e2916c475fda8 Mon Sep 17 00:00:00 2001 From: Stefan Medack Date: Sat, 30 Dec 2017 22:44:33 +0100 Subject: [PATCH 04/16] fixes initial loading after updating support libs --- .../main/java/de/stefanmedack/ccctv/ui/main/MainFragment.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainFragment.kt index 1609e9a..e12d1d9 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainFragment.kt @@ -38,6 +38,7 @@ class MainFragment : BrowseSupportFragment() { AndroidSupportInjection.inject(this) super.onCreate(savedInstanceState) + prepareEntranceTransition() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -53,8 +54,6 @@ class MainFragment : BrowseSupportFragment() { } private fun setupUi(context: Context) { - prepareEntranceTransition() - headersState = BrowseFragment.HEADERS_ENABLED isHeadersTransitionOnBackEnabled = true badgeDrawable = ContextCompat.getDrawable(context, R.drawable.voctocat) From d97e568ac5900d3f84fa56e11cd539e8d3a5377a Mon Sep 17 00:00:00 2001 From: Stefan Medack Date: Mon, 1 Jan 2018 15:58:20 +0100 Subject: [PATCH 05/16] improves seeking and splits exo player logic in ExoPlayerAdapter into BaseExoPlayerAdapter --- CHANGELOG.md | 4 + .../ccctv/ui/detail/DetailFragment.kt | 3 +- .../detail/playback/BaseExoPlayerAdapter.kt | 239 +++++++++++++++ .../ui/detail/playback/ExoPlayerAdapter.kt | 287 +++--------------- .../detail/playback/VideoMediaPlayerGlue.kt | 12 +- 5 files changed, 299 insertions(+), 246 deletions(-) create mode 100644 app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/BaseExoPlayerAdapter.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fd6ad5..72d1ccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## Unreleased + +- improves seeking workaround + ## 2.0.0 - adds streaming diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailFragment.kt index 99e93e8..53cc716 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailFragment.kt @@ -138,11 +138,12 @@ class DetailFragment : DetailsSupportFragment() { activity?.let { activityContext -> val playerAdapter = ExoPlayerAdapter(activityContext) + playerAdapter.bindRecordings(viewModel.eventWithRecordings) + val mediaPlayerGlue = VideoMediaPlayerGlue(activityContext, playerAdapter) mediaPlayerGlue.isSeekEnabled = true mediaPlayerGlue.title = result.event.title mediaPlayerGlue.subtitle = result.event.subtitle - mediaPlayerGlue.playerAdapter.bindRecordings(viewModel.eventWithRecordings) detailsBackground.enableParallax() detailsBackground.setupVideoPlayback(mediaPlayerGlue) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/BaseExoPlayerAdapter.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/BaseExoPlayerAdapter.kt new file mode 100644 index 0000000..0e36e59 --- /dev/null +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/BaseExoPlayerAdapter.kt @@ -0,0 +1,239 @@ +package de.stefanmedack.ccctv.ui.detail.playback + +import android.content.Context +import android.net.Uri +import android.os.Handler +import android.support.v17.leanback.media.PlaybackGlueHost +import android.support.v17.leanback.media.PlayerAdapter +import android.support.v17.leanback.media.SurfaceHolderGlueHost +import android.view.SurfaceHolder +import com.google.android.exoplayer2.* +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory +import com.google.android.exoplayer2.source.ExtractorMediaSource +import com.google.android.exoplayer2.source.MediaSource +import com.google.android.exoplayer2.source.TrackGroupArray +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector +import com.google.android.exoplayer2.trackselection.TrackSelectionArray +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory +import com.google.android.exoplayer2.util.Util +import de.stefanmedack.ccctv.R + + +open class BaseExoPlayerAdapter(private val context: Context) : PlayerAdapter(), Player.EventListener { + + val updatePeriod = 16L + + val player: SimpleExoPlayer = ExoPlayerFactory.newSimpleInstance( + DefaultRenderersFactory(context), + DefaultTrackSelector(), + DefaultLoadControl()) + .also { + it.addListener(this) + } + internal var surfaceHolderGlueHost: SurfaceHolderGlueHost? = null + internal val runnable: Runnable = object : Runnable { + override fun run() { + callback.onCurrentPositionChanged(this@BaseExoPlayerAdapter) + callback.onBufferedPositionChanged(this@BaseExoPlayerAdapter) + handler.postDelayed(this, updatePeriod) + } + } + internal val handler = Handler() + internal var initialized = false + internal var hasDisplay: Boolean = false + internal var bufferingStart: Boolean = false + + var mediaSourceUri: Uri? = null + + override fun onAttachedToHost(host: PlaybackGlueHost?) { + if (host is SurfaceHolderGlueHost) { + surfaceHolderGlueHost = host + surfaceHolderGlueHost?.setSurfaceHolderCallback(VideoPlayerSurfaceHolderCallback()) + } + } + + /** + * Will reset the [ExoPlayer] and the glue such that a new file can be played. You are + * not required to call this method before playing the first file. However you have to call it + * before playing a second one. + */ + internal fun reset() { + changeToUninitialized() + player.stop() + } + + private fun changeToUninitialized() { + if (initialized) { + initialized = false + notifyBufferingStartEnd() + if (hasDisplay) { + callback.onPreparedStateChanged(this@BaseExoPlayerAdapter) + } + } + } + + /** + * Notify the state of buffering. For example, an app may enable/disable a loading figure + * according to the state of buffering. + */ + internal fun notifyBufferingStartEnd() { + callback.onBufferingStateChanged(this@BaseExoPlayerAdapter, bufferingStart || !initialized) + } + + /** + * Release internal [SimpleExoPlayer]. Should not use the object after call release(). + */ + private fun release() { + changeToUninitialized() + hasDisplay = false + player.release() + } + + override fun onDetachedFromHost() { + if (surfaceHolderGlueHost != null) { + surfaceHolderGlueHost?.setSurfaceHolderCallback(null) + surfaceHolderGlueHost = null + } + reset() + release() + } + + /** + * @see SimpleExoPlayer.setVideoSurfaceHolder + */ + internal fun setDisplay(surfaceHolder: SurfaceHolder?) { + val hadDisplay = hasDisplay + hasDisplay = surfaceHolder != null + if (hadDisplay == hasDisplay) { + return + } + + player.setVideoSurfaceHolder(surfaceHolder) + if (hasDisplay) { + if (initialized) { + callback.onPreparedStateChanged(this@BaseExoPlayerAdapter) + } + } else { + if (initialized) { + callback.onPreparedStateChanged(this@BaseExoPlayerAdapter) + } + } + } + + override fun setProgressUpdatingEnabled(enabled: Boolean) { + handler.removeCallbacks(runnable) + if (!enabled) { + return + } + handler.postDelayed(runnable, updatePeriod) + } + + override fun play() { + if (!initialized || isPlaying) { + return + } + + player.playWhenReady = true + callback.onPlayStateChanged(this@BaseExoPlayerAdapter) + callback.onCurrentPositionChanged(this@BaseExoPlayerAdapter) + } + + override fun pause() { + if (isPlaying) { + player.playWhenReady = false + callback.onPlayStateChanged(this@BaseExoPlayerAdapter) + } + } + + override fun seekTo(newPosition: Long) { + if (initialized) { + player.seekTo(newPosition) + } + } + + override fun isPlaying(): Boolean = initialized && player.playbackState == Player.STATE_READY && player.playWhenReady + + override fun getDuration(): Long = if (initialized) player.duration else -1 + + override fun getCurrentPosition(): Long = if (initialized) player.currentPosition else -1 + + override fun getBufferedPosition(): Long = player.bufferedPosition + + /** + * Set [MediaSource] for [SimpleExoPlayer]. An app may override this method in order + * to use different [MediaSource]. + * @param uri The url of media source + * * + * @return MediaSource for the player + */ + internal fun onCreateMediaSource(uri: Uri): MediaSource { + val userAgent = Util.getUserAgent(context, "BaseExoPlayerAdapter") + return ExtractorMediaSource(uri, + DefaultHttpDataSourceFactory(userAgent, null, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, + DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, true), + DefaultExtractorsFactory(), null, null) + } + + /** + * @return True if ExoPlayer is ready and got a SurfaceHolder if + * * [PlaybackGlueHost] provides SurfaceHolder. + */ + override fun isPrepared(): Boolean { + return initialized && (surfaceHolderGlueHost == null || hasDisplay) + } + + /** + * Implements [SurfaceHolder.Callback] that can then be set on the + * [PlaybackGlueHost]. + */ + internal inner class VideoPlayerSurfaceHolderCallback : SurfaceHolder.Callback { + override fun surfaceCreated(surfaceHolder: SurfaceHolder) { + setDisplay(surfaceHolder) + } + + override fun surfaceChanged(surfaceHolder: SurfaceHolder, i: Int, i1: Int, i2: Int) {} + + override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) { + setDisplay(null) + } + } + + // ExoPlayer Event Listeners + + override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { + bufferingStart = false + if (playbackState == Player.STATE_READY && !initialized) { + initialized = true + if (surfaceHolderGlueHost == null || hasDisplay) { + callback.onPreparedStateChanged(this@BaseExoPlayerAdapter) + } + } else if (playbackState == Player.STATE_BUFFERING) { + bufferingStart = true + } else if (playbackState == Player.STATE_ENDED) { + callback.onPlayStateChanged(this@BaseExoPlayerAdapter) + callback.onPlayCompleted(this@BaseExoPlayerAdapter) + } + notifyBufferingStartEnd() + } + + override fun onPlayerError(error: ExoPlaybackException) { + callback.onError( + this@BaseExoPlayerAdapter, + error.type, + context.getString(R.string.lb_media_player_error, error.type, error.rendererIndex) + ) + } + + override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {} + + override fun onLoadingChanged(isLoading: Boolean) {} + + override fun onPositionDiscontinuity() {} + + override fun onTimelineChanged(timeline: Timeline?, manifest: Any?) {} + + override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {} + + override fun onRepeatModeChanged(repeatMode: Int) {} +} diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/ExoPlayerAdapter.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/ExoPlayerAdapter.kt index 7f3bf5b..cc2600b 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/ExoPlayerAdapter.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/ExoPlayerAdapter.kt @@ -1,23 +1,10 @@ package de.stefanmedack.ccctv.ui.detail.playback +import android.app.Instrumentation import android.content.Context import android.net.Uri -import android.os.Handler -import android.support.v17.leanback.media.PlaybackGlueHost -import android.support.v17.leanback.media.PlayerAdapter -import android.support.v17.leanback.media.SurfaceHolderGlueHost -import android.view.SurfaceHolder -import com.google.android.exoplayer2.* -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory -import com.google.android.exoplayer2.source.ExtractorMediaSource -import com.google.android.exoplayer2.source.MediaSource -import com.google.android.exoplayer2.source.TrackGroupArray -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector -import com.google.android.exoplayer2.trackselection.TrackSelectionArray -import com.google.android.exoplayer2.upstream.DefaultHttpDataSource -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory -import com.google.android.exoplayer2.util.Util -import de.stefanmedack.ccctv.R +import android.view.KeyEvent +import com.google.android.exoplayer2.SimpleExoPlayer import de.stefanmedack.ccctv.repository.EventRemote import de.stefanmedack.ccctv.util.bestRecording import de.stefanmedack.ccctv.util.switchAspectRatio @@ -27,166 +14,22 @@ import info.metadude.kotlin.library.c3media.models.Recording import io.reactivex.Single import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.subscribeBy +import timber.log.Timber import java.util.* -class ExoPlayerAdapter(private val context: Context) : PlayerAdapter(), Player.EventListener { - - val updatePeriod = 16L - - private val player: SimpleExoPlayer = ExoPlayerFactory.newSimpleInstance( - DefaultRenderersFactory(context), - DefaultTrackSelector(), - DefaultLoadControl()) - .also { - it.addListener(this) - } - private var surfaceHolderGlueHost: SurfaceHolderGlueHost? = null - private val runnable: Runnable = object : Runnable { - override fun run() { - callback.onCurrentPositionChanged(this@ExoPlayerAdapter) - callback.onBufferedPositionChanged(this@ExoPlayerAdapter) - handler.postDelayed(this, updatePeriod) - } - } - private val handler = Handler() - private var initialized = false - private var hasDisplay: Boolean = false - private var bufferingStart: Boolean = false +class ExoPlayerAdapter(context: Context) : BaseExoPlayerAdapter(context) { private var event: Event? = null private var shouldUseHighQuality = true private var bestRecording: Recording? = null private var currentRecordingWidth: Int? = null private var currentRecordingHeight: Int? = null - private var mediaSourceUri: Uri? = null private val disposables = CompositeDisposable() - override fun onAttachedToHost(host: PlaybackGlueHost?) { - if (host is SurfaceHolderGlueHost) { - surfaceHolderGlueHost = host - surfaceHolderGlueHost?.setSurfaceHolderCallback(VideoPlayerSurfaceHolderCallback()) - } - } - - /** - * Will reset the [ExoPlayer] and the glue such that a new file can be played. You are - * not required to call this method before playing the first file. However you have to call it - * before playing a second one. - */ - private fun reset() { - changeToUninitialized() - player.stop() - } - - private fun changeToUninitialized() { - if (initialized) { - initialized = false - notifyBufferingStartEnd() - if (hasDisplay) { - callback.onPreparedStateChanged(this@ExoPlayerAdapter) - } - } - } - - /** - * Notify the state of buffering. For example, an app may enable/disable a loading figure - * according to the state of buffering. - */ - private fun notifyBufferingStartEnd() { - callback.onBufferingStateChanged(this@ExoPlayerAdapter, - bufferingStart || !initialized) - } - - /** - * Release internal [SimpleExoPlayer]. Should not use the object after call release(). - */ - private fun release() { - changeToUninitialized() - hasDisplay = false - player.release() - } - override fun onDetachedFromHost() { disposables.clear() - if (surfaceHolderGlueHost != null) { - surfaceHolderGlueHost?.setSurfaceHolderCallback(null) - surfaceHolderGlueHost = null - } - reset() - release() - } - - /** - * @see SimpleExoPlayer.setVideoSurfaceHolder - */ - internal fun setDisplay(surfaceHolder: SurfaceHolder?) { - val hadDisplay = hasDisplay - hasDisplay = surfaceHolder != null - if (hadDisplay == hasDisplay) { - return - } - - player.setVideoSurfaceHolder(surfaceHolder) - if (hasDisplay) { - if (initialized) { - callback.onPreparedStateChanged(this@ExoPlayerAdapter) - } - } else { - if (initialized) { - callback.onPreparedStateChanged(this@ExoPlayerAdapter) - } - } - } - - override fun setProgressUpdatingEnabled(enabled: Boolean) { - handler.removeCallbacks(runnable) - if (!enabled) { - return - } - handler.postDelayed(runnable, updatePeriod) - } - - override fun isPlaying(): Boolean { - val exoPlayerIsPlaying = player.playbackState == Player.STATE_READY && player.playWhenReady - return initialized && exoPlayerIsPlaying - } - - override fun getDuration(): Long { - return if (initialized) player.duration else -1 - } - - override fun getCurrentPosition(): Long { - return if (initialized) player.currentPosition else -1 - } - - - override fun play() { - if (!initialized || isPlaying) { - return - } - - player.playWhenReady = true - callback.onPlayStateChanged(this@ExoPlayerAdapter) - callback.onCurrentPositionChanged(this@ExoPlayerAdapter) - } - - override fun pause() { - if (isPlaying) { - player.playWhenReady = false - callback.onPlayStateChanged(this@ExoPlayerAdapter) - } - } - - override fun seekTo(newPosition: Long) { - if (!initialized) { - return - } - player.seekTo(newPosition) - } - - override fun getBufferedPosition(): Long { - return player.bufferedPosition + super.onDetachedFromHost() } fun bindRecordings(recordings: Single) { @@ -220,34 +63,6 @@ class ExoPlayerAdapter(private val context: Context) : PlayerAdapter(), Player.E } } - private fun extractBestRecording(ev: Event) { - bestRecording = ev.bestRecording( - if (ev.originalLanguage.isEmpty()) - Language.toLanguage(Locale.getDefault().isO3Country) - else - ev.originalLanguage.first(), - shouldUseHighQuality - ) - currentRecordingHeight = bestRecording?.height - currentRecordingWidth = bestRecording?.width - mediaSourceUri = Uri.parse(bestRecording?.recordingUrl) - } - - /** - * Set [MediaSource] for [SimpleExoPlayer]. An app may override this method in order - * to use different [MediaSource]. - * @param uri The url of media source - * * - * @return MediaSource for the player - */ - private fun onCreateMediaSource(uri: Uri): MediaSource { - val userAgent = Util.getUserAgent(context, "ExoPlayerAdapter") - return ExtractorMediaSource(uri, - DefaultHttpDataSourceFactory(userAgent, null, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, - DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, true), - DefaultExtractorsFactory(), null, null) - } - private fun prepareMediaForPlaying() { reset() @@ -272,65 +87,59 @@ class ExoPlayerAdapter(private val context: Context) : PlayerAdapter(), Player.E callback.onPlayStateChanged(this@ExoPlayerAdapter) } - /** - * @return True if ExoPlayer is ready and got a SurfaceHolder if - * * [PlaybackGlueHost] provides SurfaceHolder. - */ - override fun isPrepared(): Boolean { - return initialized && (surfaceHolderGlueHost == null || hasDisplay) + private fun extractBestRecording(ev: Event) { + bestRecording = ev.bestRecording( + if (ev.originalLanguage.isEmpty()) + Language.toLanguage(Locale.getDefault().isO3Country) + else + ev.originalLanguage.first(), + shouldUseHighQuality + ) + currentRecordingHeight = bestRecording?.height + currentRecordingWidth = bestRecording?.width + mediaSourceUri = Uri.parse(bestRecording?.recordingUrl) } - /** - * Implements [SurfaceHolder.Callback] that can then be set on the - * [PlaybackGlueHost]. - */ - internal inner class VideoPlayerSurfaceHolderCallback : SurfaceHolder.Callback { - override fun surfaceCreated(surfaceHolder: SurfaceHolder) { - setDisplay(surfaceHolder) - } + // This is a workaround to simulate a Dpad enter key after seeking. As long as the SeekProvider with video thumbnails is not implemented + // (see https://developer.android.com/reference/android/support/v17/leanback/media/PlaybackTransportControlGlue.html#setSeekProvider ), + // the default behaviour for seeking implemented by PlaybackTransportControlGlue is quite confusing to the user. + // + // Any better solutions than this one are gladly accepted - override fun surfaceChanged(surfaceHolder: SurfaceHolder, i: Int, i1: Int, i2: Int) {} + private val DPAD_ENTER_KEY_DELAY = 2500L - override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) { - setDisplay(null) + private var shouldTriggerDpadCenterKeyEvent = false + private val triggerDpadCenterKeyRunnable = Runnable { + if (shouldTriggerDpadCenterKeyEvent) { + shouldTriggerDpadCenterKeyEvent = false + triggerDpadCenterKeyEvent() } } - // ExoPlayer Event Listeners - - override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { - bufferingStart = false - if (playbackState == Player.STATE_READY && !initialized) { - initialized = true - if (surfaceHolderGlueHost == null || hasDisplay) { - callback.onPreparedStateChanged(this@ExoPlayerAdapter) - } - } else if (playbackState == Player.STATE_BUFFERING) { - bufferingStart = true - } else if (playbackState == Player.STATE_ENDED) { - callback.onPlayStateChanged(this@ExoPlayerAdapter) - callback.onPlayCompleted(this@ExoPlayerAdapter) + override fun seekTo(newPosition: Long) { + super.seekTo(newPosition) + if (initialized) { + seekingWorkaround() } - notifyBufferingStartEnd() } - override fun onPlayerError(error: ExoPlaybackException) { - callback.onError( - this@ExoPlayerAdapter, - error.type, - context.getString(R.string.lb_media_player_error, error.type, error.rendererIndex) - ) + private fun seekingWorkaround() { + shouldTriggerDpadCenterKeyEvent = true + handler.removeCallbacks(triggerDpadCenterKeyRunnable) + handler.postDelayed(triggerDpadCenterKeyRunnable, DPAD_ENTER_KEY_DELAY) } - override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {} - - override fun onLoadingChanged(isLoading: Boolean) {} - - override fun onPositionDiscontinuity() {} - - override fun onTimelineChanged(timeline: Timeline?, manifest: Any?) {} + private fun triggerDpadCenterKeyEvent() { + Thread({ + try { + val inst = Instrumentation() + inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER) + } catch (e: InterruptedException) { + Timber.w(e, "Could not simulate KEYCODE_ENTER") + } + }).start() + } - override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {} + // end workaround for seeking - override fun onRepeatModeChanged(repeatMode: Int) {} -} +} \ No newline at end of file diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/VideoMediaPlayerGlue.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/VideoMediaPlayerGlue.kt index 430e501..91e5b3c 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/VideoMediaPlayerGlue.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/VideoMediaPlayerGlue.kt @@ -10,7 +10,8 @@ import android.support.v17.leanback.widget.PlaybackControlsRow.HighQualityAction import android.support.v17.leanback.widget.PlaybackControlsRow.HighQualityAction.INDEX_ON import de.stefanmedack.ccctv.ui.detail.playback.actions.AspectRatioAction -class VideoMediaPlayerGlue(activity: Activity, impl: T) : PlaybackTransportControlGlue(activity, impl) { +class VideoMediaPlayerGlue(activity: Activity, playerAdapter: T) + : PlaybackTransportControlGlue(activity, playerAdapter) { private val pipAction = PlaybackControlsRow.PictureInPictureAction(activity) private val highQualityAction = PlaybackControlsRow.HighQualityAction(activity).apply { index = INDEX_ON } @@ -34,11 +35,10 @@ class VideoMediaPlayerGlue(activity: Activity, impl: T) : Pla super.onActionClicked(action) } - private fun shouldDispatchAction(action: Action): Boolean { - return action == pipAction - || action == highQualityAction - || action == aspectRatioAction - } + private fun shouldDispatchAction(action: Action): Boolean = + action == pipAction + || action == highQualityAction + || action == aspectRatioAction private fun dispatchAction(action: Action) { when (action) { From 29b26862f4d155c30888b4738419d6bf37debfda Mon Sep 17 00:00:00 2001 From: Stefan Medack Date: Mon, 1 Jan 2018 16:04:25 +0100 Subject: [PATCH 06/16] connects dpad_enter key event for seeking workaround to the point where buffering has finished --- .../ui/detail/playback/ExoPlayerAdapter.kt | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/ExoPlayerAdapter.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/ExoPlayerAdapter.kt index cc2600b..4997b22 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/ExoPlayerAdapter.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/ExoPlayerAdapter.kt @@ -4,6 +4,7 @@ import android.app.Instrumentation import android.content.Context import android.net.Uri import android.view.KeyEvent +import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.SimpleExoPlayer import de.stefanmedack.ccctv.repository.EventRemote import de.stefanmedack.ccctv.util.bestRecording @@ -106,27 +107,22 @@ class ExoPlayerAdapter(context: Context) : BaseExoPlayerAdapter(context) { // // Any better solutions than this one are gladly accepted - private val DPAD_ENTER_KEY_DELAY = 2500L - private var shouldTriggerDpadCenterKeyEvent = false - private val triggerDpadCenterKeyRunnable = Runnable { - if (shouldTriggerDpadCenterKeyEvent) { - shouldTriggerDpadCenterKeyEvent = false - triggerDpadCenterKeyEvent() - } - } override fun seekTo(newPosition: Long) { super.seekTo(newPosition) if (initialized) { - seekingWorkaround() + shouldTriggerDpadCenterKeyEvent = true } } - private fun seekingWorkaround() { - shouldTriggerDpadCenterKeyEvent = true - handler.removeCallbacks(triggerDpadCenterKeyRunnable) - handler.postDelayed(triggerDpadCenterKeyRunnable, DPAD_ENTER_KEY_DELAY) + override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { + super.onPlayerStateChanged(playWhenReady, playbackState) + + if (shouldTriggerDpadCenterKeyEvent && playbackState == Player.STATE_READY) { + shouldTriggerDpadCenterKeyEvent = false + triggerDpadCenterKeyEvent() + } } private fun triggerDpadCenterKeyEvent() { From 44db1fe59af8a54b4d03e1b6660f259f56b6b69e Mon Sep 17 00:00:00 2001 From: Stefan Medack Date: Mon, 1 Jan 2018 19:44:56 +0100 Subject: [PATCH 07/16] adds initial version to display all conferences as a Grid on MainView and hide events one layer deeper --- .../ccctv/persistence/ConferenceDaoTest.kt | 16 +- .../ccctv/di/modules/ViewModelModule.kt | 6 +- .../ccctv/persistence/daos/ConferenceDao.kt | 3 + .../ccctv/repository/ConferenceRepository.kt | 47 ++-- .../ccctv/ui/about/AboutFragment.kt | 7 +- .../ccctv/ui/base/GridFragment.java | 211 ++++++++++++++++++ .../ccctv/ui/cards/ConferenceCardPresenter.kt | 63 ++++++ ...ncesFragment.kt => ConferencesFragment.kt} | 63 ++++-- ...esViewModel.kt => ConferencesViewModel.kt} | 10 +- .../ccctv/ui/main/LiveStreamingViewModel.kt | 2 +- .../ccctv/ui/main/MainFragment.kt | 2 +- .../stefanmedack/ccctv/ui/main/MainModule.kt | 2 +- .../ccctv/ui/main/MainViewModel.kt | 8 +- app/src/main/res/layout/grid_fragment.xml | 21 ++ 14 files changed, 395 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/de/stefanmedack/ccctv/ui/base/GridFragment.java create mode 100644 app/src/main/java/de/stefanmedack/ccctv/ui/cards/ConferenceCardPresenter.kt rename app/src/main/java/de/stefanmedack/ccctv/ui/main/{GroupedConferencesFragment.kt => ConferencesFragment.kt} (54%) rename app/src/main/java/de/stefanmedack/ccctv/ui/main/{GroupedConferencesViewModel.kt => ConferencesViewModel.kt} (70%) create mode 100644 app/src/main/res/layout/grid_fragment.xml diff --git a/app/src/androidTest/java/de/stefanmedack/ccctv/persistence/ConferenceDaoTest.kt b/app/src/androidTest/java/de/stefanmedack/ccctv/persistence/ConferenceDaoTest.kt index 7bc8d02..4b22f02 100644 --- a/app/src/androidTest/java/de/stefanmedack/ccctv/persistence/ConferenceDaoTest.kt +++ b/app/src/androidTest/java/de/stefanmedack/ccctv/persistence/ConferenceDaoTest.kt @@ -66,6 +66,20 @@ class ConferenceDaoTest : BaseDbTest() { assertEquals(loadedConferences[0], newConference) } + @Test + fun insert_and_retrieve_multiple_conferences_filtered_by_group() { + val conferences = listOf( + minimalConferenceEntity.copy(id = 1, slug = "congress/33c3"), + fullConferenceEntity.copy(id = 2, slug = "not_congress/droidcon") + ) + + db.conferenceDao().insertAll(conferences) + val loadedConferences = db.conferenceDao().getConferences("congress").test().values()[0] + + assertEquals(loadedConferences.size, 1) + assertEquals(loadedConferences[0], conferences[0]) + } + // Conferences with Events @Test @@ -103,7 +117,7 @@ class ConferenceDaoTest : BaseDbTest() { assertEquals(loadedConferences[1].events, listOf()) } - @Test + @Test fun insert_and_retrieve_multiple_conferences_with_events_in_single_insert() { val conferences = listOf( minimalConferenceEntity.copy(id = 1), diff --git a/app/src/main/java/de/stefanmedack/ccctv/di/modules/ViewModelModule.kt b/app/src/main/java/de/stefanmedack/ccctv/di/modules/ViewModelModule.kt index c4ffd93..57d05c8 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/di/modules/ViewModelModule.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/di/modules/ViewModelModule.kt @@ -8,7 +8,7 @@ import dagger.multibindings.IntoMap import de.stefanmedack.ccctv.di.C3ViewModelFactory import de.stefanmedack.ccctv.di.Scopes.ViewModelKey import de.stefanmedack.ccctv.ui.detail.DetailViewModel -import de.stefanmedack.ccctv.ui.main.GroupedConferencesViewModel +import de.stefanmedack.ccctv.ui.main.ConferencesViewModel import de.stefanmedack.ccctv.ui.main.LiveStreamingViewModel import de.stefanmedack.ccctv.ui.main.MainViewModel import de.stefanmedack.ccctv.ui.search.SearchViewModel @@ -23,8 +23,8 @@ abstract class ViewModelModule { @Binds @IntoMap - @ViewModelKey(GroupedConferencesViewModel::class) - abstract fun bindGroupedConferencesViewModel(viewModel: GroupedConferencesViewModel): ViewModel + @ViewModelKey(ConferencesViewModel::class) + abstract fun bindConferencesViewModel(viewModel: ConferencesViewModel): ViewModel @Binds @IntoMap diff --git a/app/src/main/java/de/stefanmedack/ccctv/persistence/daos/ConferenceDao.kt b/app/src/main/java/de/stefanmedack/ccctv/persistence/daos/ConferenceDao.kt index 951d911..7e07383 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/persistence/daos/ConferenceDao.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/persistence/daos/ConferenceDao.kt @@ -15,6 +15,9 @@ interface ConferenceDao { @Query("SELECT * FROM Conferences") fun getConferences(): Flowable> + @Query("SELECT * FROM Conferences WHERE slug LIKE :conferenceGroup || '%'") + fun getConferences(conferenceGroup: String): Flowable> + @Query("SELECT * FROM Conferences") fun getConferencesWithEvents(): Flowable> diff --git a/app/src/main/java/de/stefanmedack/ccctv/repository/ConferenceRepository.kt b/app/src/main/java/de/stefanmedack/ccctv/repository/ConferenceRepository.kt index 04a2908..a1b7331 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/repository/ConferenceRepository.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/repository/ConferenceRepository.kt @@ -20,27 +20,26 @@ class ConferenceRepository @Inject constructor( private val conferenceDao: ConferenceDao, private val preferences: C3SharedPreferences ) { - // TODO are plain conferences still needed? - // val conferences: Flowable>> - // get() = object : NetworkBoundResource, List>() { - // - // override fun fetchLocal(): Flowable> = conferenceDao.getConferences() - // - // override fun saveLocal(data: List) = conferenceDao.insertAll(data) - // - // override fun isStale(localResource: Resource>) = when (localResource) { - // is Resource.Error -> true - // is Resource.Loading -> false - // is Resource.Success -> localResource.data.isEmpty() - // } - // - // override fun fetchNetwork(): Single> = mediaService - // .getConferences() - // .map { it.conferences?.filterNotNull() } - // - // override fun mapNetworkToLocal(data: List) = data.mapNotNull { it.toEntity() } - // - // }.resource + val conferences: Flowable>> + get() = object : NetworkBoundResource, List>() { + + override fun fetchLocal(): Flowable> = conferenceDao.getConferences() + + override fun saveLocal(data: List) = conferenceDao.insertAll(data) + + override fun isStale(localResource: Resource>) = when (localResource) { + is Resource.Error -> true + is Resource.Loading -> false + is Resource.Success -> localResource.data.isEmpty() + } + + override fun fetchNetwork(): Single> = mediaService + .getConferences() + .map { it.conferences?.filterNotNull() } + + override fun mapNetworkToLocal(data: List) = data.mapNotNull { it.toEntity() } + + }.resource val conferencesWithEvents: Flowable>> get() = object : NetworkBoundResource, List>() { @@ -84,9 +83,9 @@ class ConferenceRepository @Inject constructor( }.resource - fun loadedConferences(conferenceGroup: String): Flowable>> = conferenceDao - .getConferencesWithEvents(conferenceGroup.toLowerCase()) - .map>> { Resource.Success(it) } + fun loadedConferences(conferenceGroup: String): Flowable>> = conferenceDao + .getConferences(conferenceGroup.toLowerCase()) + .map>> { Resource.Success(it) } .applySchedulers() } diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/about/AboutFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/about/AboutFragment.kt index 00297e5..947a034 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/about/AboutFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/about/AboutFragment.kt @@ -23,10 +23,10 @@ class AboutFragment : DetailsSupportFragment(), BrowseSupportFragment.MainFragme var shouldKeyLeftEventTriggerBackAnimation = true var shouldKeyUpEventTriggerBackAnimation = false - private val mMainFragmentAdapter = BrowseSupportFragment.MainFragmentAdapter(this) + private val mainFragmentAdapter = BrowseSupportFragment.MainFragmentAdapter(this) override fun getMainFragmentAdapter(): BrowseSupportFragment.MainFragmentAdapter<*> { - return mMainFragmentAdapter + return mainFragmentAdapter } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -39,8 +39,7 @@ class AboutFragment : DetailsSupportFragment(), BrowseSupportFragment.MainFragme private fun setupUi(context: Context) { DetailsSupportFragmentBackgroundController(this).apply { enableParallax() - coverBitmap = BitmapFactory.decodeResource(resources, - R.drawable.about_cover) + coverBitmap = BitmapFactory.decodeResource(resources, R.drawable.about_cover) } val detailOverviewRowPresenter = FullWidthDetailsOverviewRowPresenter(AboutDescriptionPresenter()) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/base/GridFragment.java b/app/src/main/java/de/stefanmedack/ccctv/ui/base/GridFragment.java new file mode 100644 index 0000000..c4165eb --- /dev/null +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/base/GridFragment.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package de.stefanmedack.ccctv.ui.base; + +import android.os.Bundle; +import android.support.v17.leanback.app.BrowseSupportFragment; +import android.support.v17.leanback.widget.ObjectAdapter; +import android.support.v17.leanback.widget.OnChildLaidOutListener; +import android.support.v17.leanback.widget.OnItemViewClickedListener; +import android.support.v17.leanback.widget.OnItemViewSelectedListener; +import android.support.v17.leanback.widget.Presenter; +import android.support.v17.leanback.widget.Row; +import android.support.v17.leanback.widget.RowPresenter; +import android.support.v17.leanback.widget.VerticalGridPresenter; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import de.stefanmedack.ccctv.R; + +/** + * A fragment for rendering items in a vertical grids. + */ +// TODO use VerticalGridSupportFragment with BrowseSupportFragment.MainFragmentAdapterProvider instead +public class GridFragment extends Fragment implements BrowseSupportFragment.MainFragmentAdapterProvider { + private static final String TAG = "VerticalGridFragment"; + private static boolean DEBUG = false; + + private ObjectAdapter mAdapter; + private VerticalGridPresenter mGridPresenter; + private VerticalGridPresenter.ViewHolder mGridViewHolder; + private OnItemViewSelectedListener mOnItemViewSelectedListener; + private OnItemViewClickedListener mOnItemViewClickedListener; + private int mSelectedPosition = -1; + private BrowseSupportFragment.MainFragmentAdapter mMainFragmentAdapter = + new BrowseSupportFragment.MainFragmentAdapter(this) { + @Override + public void setEntranceTransitionState(boolean state) { + GridFragment.this.setEntranceTransitionState(state); + } + }; + /** + * Sets the grid presenter. + */ + public void setGridPresenter(VerticalGridPresenter gridPresenter) { + if (gridPresenter == null) { + throw new IllegalArgumentException("Grid presenter may not be null"); + } + mGridPresenter = gridPresenter; + mGridPresenter.setOnItemViewSelectedListener(mViewSelectedListener); + if (mOnItemViewClickedListener != null) { + mGridPresenter.setOnItemViewClickedListener(mOnItemViewClickedListener); + } + } + + /** + * Returns the grid presenter. + */ + public VerticalGridPresenter getGridPresenter() { + return mGridPresenter; + } + + /** + * Sets the object adapter for the fragment. + */ + public void setAdapter(ObjectAdapter adapter) { + mAdapter = adapter; + updateAdapter(); + } + + /** + * Returns the object adapter. + */ + public ObjectAdapter getAdapter() { + return mAdapter; + } + + final private OnItemViewSelectedListener mViewSelectedListener = + new OnItemViewSelectedListener() { + @Override + public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, + RowPresenter.ViewHolder rowViewHolder, Row row) { + int position = mGridViewHolder.getGridView().getSelectedPosition(); + if (DEBUG) Log.v(TAG, "grid selected position " + position); + gridOnItemSelected(position); + if (mOnItemViewSelectedListener != null) { + mOnItemViewSelectedListener.onItemSelected(itemViewHolder, item, + rowViewHolder, row); + } + } + }; + + final private OnChildLaidOutListener mChildLaidOutListener = + new OnChildLaidOutListener() { + @Override + public void onChildLaidOut(ViewGroup parent, View view, int position, long id) { + if (position == 0) { + showOrHideTitle(); + } + } + }; + + /** + * Sets an item selection listener. + */ + public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { + mOnItemViewSelectedListener = listener; + } + + private void gridOnItemSelected(int position) { + if (position != mSelectedPosition) { + mSelectedPosition = position; + showOrHideTitle(); + } + } + + private void showOrHideTitle() { + if (mGridViewHolder.getGridView().findViewHolderForAdapterPosition(mSelectedPosition) == null) { + return; + } + if (!mGridViewHolder.getGridView().hasPreviousViewInSameRow(mSelectedPosition)) { + mMainFragmentAdapter.getFragmentHost().showTitleView(true); + } else { + mMainFragmentAdapter.getFragmentHost().showTitleView(false); + } + } + + /** + * Sets an item clicked listener. + */ + public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { + mOnItemViewClickedListener = listener; + if (mGridPresenter != null) { + mGridPresenter.setOnItemViewClickedListener(mOnItemViewClickedListener); + } + } + + /** + * Returns the item clicked listener. + */ + public OnItemViewClickedListener getOnItemViewClickedListener() { + return mOnItemViewClickedListener; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.grid_fragment, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ViewGroup gridDock = (ViewGroup) view.findViewById(R.id.browse_grid_dock); + mGridViewHolder = mGridPresenter.onCreateViewHolder(gridDock); + gridDock.addView(mGridViewHolder.view); + mGridViewHolder.getGridView().setOnChildLaidOutListener(mChildLaidOutListener); + + getMainFragmentAdapter().getFragmentHost().notifyViewCreated(mMainFragmentAdapter); + updateAdapter(); + + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mGridViewHolder = null; + } + + @Override + public BrowseSupportFragment.MainFragmentAdapter getMainFragmentAdapter() { + return mMainFragmentAdapter; + } + + /** + * Sets the selected item position. + */ + public void setSelectedPosition(int position) { + mSelectedPosition = position; + if(mGridViewHolder != null && mGridViewHolder.getGridView().getAdapter() != null) { + mGridViewHolder.getGridView().setSelectedPositionSmooth(position); + } + } + + private void updateAdapter() { + if (mGridViewHolder != null) { + mGridPresenter.onBindViewHolder(mGridViewHolder, mAdapter); + if (mSelectedPosition != -1) { + mGridViewHolder.getGridView().setSelectedPosition(mSelectedPosition); + } + } + } + + void setEntranceTransitionState(boolean afterTransition) { + mGridPresenter.setEntranceTransitionState(mGridViewHolder, afterTransition); + } +} diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/cards/ConferenceCardPresenter.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/cards/ConferenceCardPresenter.kt new file mode 100644 index 0000000..5ecad2d --- /dev/null +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/cards/ConferenceCardPresenter.kt @@ -0,0 +1,63 @@ +package de.stefanmedack.ccctv.ui.cards + +import android.support.v17.leanback.widget.ImageCardView +import android.support.v17.leanback.widget.Presenter +import android.support.v4.content.ContextCompat +import android.support.v7.view.ContextThemeWrapper +import android.view.ViewGroup +import com.bumptech.glide.Glide +import de.stefanmedack.ccctv.R +import de.stefanmedack.ccctv.persistence.entities.Conference +import kotlin.properties.Delegates + +class ConferenceCardPresenter : Presenter() { + + private var selectedBackgroundColor: Int by Delegates.notNull() + private var defaultBackgroundColor: Int by Delegates.notNull() + + override fun onCreateViewHolder(parent: ViewGroup): Presenter.ViewHolder { + defaultBackgroundColor = ContextCompat.getColor(parent.context, R.color.teal_900) + selectedBackgroundColor = ContextCompat.getColor(parent.context, R.color.amber_800) + + // TODO ConferenceCardStyle? + val cardView = object : ImageCardView(ContextThemeWrapper(parent.context, R.style.EventCardStyle)) { + override fun setSelected(selected: Boolean) { + updateCardBackgroundColor(this, selected) + super.setSelected(selected) + } + } + + cardView.isFocusable = true + cardView.isFocusableInTouchMode = true + updateCardBackgroundColor(cardView, false) + return Presenter.ViewHolder(cardView) + } + + override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any) { + if (item is Conference) { + (viewHolder.view as ImageCardView).let { + it.titleText = item.title + it.contentText = item.acronym + Glide.with(viewHolder.view.context) + .load(item.logoUrl) + .centerCrop() + .error(R.drawable.voctocat) + .into(it.mainImageView) + } + } + } + + override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) { + (viewHolder.view as ImageCardView).let { + it.badgeImage = null + it.mainImage = null + } + } + + private fun updateCardBackgroundColor(view: ImageCardView, selected: Boolean) { + val color = if (selected) selectedBackgroundColor else defaultBackgroundColor + // both background colors should be set because the view's background is temporarily visible during animations. + view.setBackgroundColor(color) + view.setInfoAreaBackgroundColor(color) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/main/GroupedConferencesFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/main/ConferencesFragment.kt similarity index 54% rename from app/src/main/java/de/stefanmedack/ccctv/ui/main/GroupedConferencesFragment.kt rename to app/src/main/java/de/stefanmedack/ccctv/ui/main/ConferencesFragment.kt index 61e4ecc..ca13bd1 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/main/GroupedConferencesFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/main/ConferencesFragment.kt @@ -3,15 +3,15 @@ package de.stefanmedack.ccctv.ui.main import android.arch.lifecycle.ViewModelProvider import android.arch.lifecycle.ViewModelProviders import android.os.Bundle -import android.support.v17.leanback.app.RowsSupportFragment import android.support.v17.leanback.widget.* import android.view.View import android.widget.Toast import dagger.android.support.AndroidSupportInjection import de.stefanmedack.ccctv.model.Resource -import de.stefanmedack.ccctv.persistence.entities.ConferenceWithEvents +import de.stefanmedack.ccctv.persistence.entities.Conference import de.stefanmedack.ccctv.persistence.entities.Event -import de.stefanmedack.ccctv.ui.cards.EventCardPresenter +import de.stefanmedack.ccctv.ui.base.GridFragment +import de.stefanmedack.ccctv.ui.cards.ConferenceCardPresenter import de.stefanmedack.ccctv.ui.detail.DetailActivity import de.stefanmedack.ccctv.util.CONFERENCE_GROUP import de.stefanmedack.ccctv.util.plusAssign @@ -19,24 +19,38 @@ import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.subscribeBy import javax.inject.Inject -class GroupedConferencesFragment : RowsSupportFragment() { +class ConferencesFragment : GridFragment() { + + private val COLUMNS = 4 + private val ZOOM_FACTOR = FocusHighlight.ZOOM_FACTOR_SMALL @Inject lateinit var viewModelFactory: ViewModelProvider.Factory - private val viewModel: GroupedConferencesViewModel by lazy { - ViewModelProviders.of(this, viewModelFactory).get(GroupedConferencesViewModel::class.java).apply { + private val viewModel: ConferencesViewModel by lazy { + ViewModelProviders.of(this, viewModelFactory).get(ConferencesViewModel::class.java).apply { init(arguments?.getString(CONFERENCE_GROUP, "") ?: "") } } private val disposables = CompositeDisposable() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Note: usually we call this after data is loaded. However, for this type of Fragment it does not work properly + mainFragmentAdapter.fragmentHost.notifyDataReady(mainFragmentAdapter) + + setupUi() + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { AndroidSupportInjection.inject(this) super.onViewCreated(view, savedInstanceState) - setupUi() + // Note: usually we call this after data is loaded. However, for this type of Fragment it does not work properly + mainFragmentAdapter.fragmentHost.notifyDataReady(mainFragmentAdapter) + bindViewModel() } @@ -46,8 +60,14 @@ class GroupedConferencesFragment : RowsSupportFragment() { } private fun setupUi() { - adapter = ArrayObjectAdapter(ListRowPresenter()) + gridPresenter = VerticalGridPresenter(ZOOM_FACTOR).apply { + numberOfColumns = COLUMNS + } + + adapter = ArrayObjectAdapter(ConferenceCardPresenter()) + onItemViewClickedListener = OnItemViewClickedListener { itemViewHolder, item, _, _ -> + // TODO if (item is Event) { activity?.let { DetailActivity.start(it, item, (itemViewHolder.view as ImageCardView).mainImageView) @@ -57,7 +77,7 @@ class GroupedConferencesFragment : RowsSupportFragment() { } private fun bindViewModel() { - disposables.add(viewModel.conferencesWithEvents + disposables.add(viewModel.conferences .subscribeBy( onNext = { render(it) }, onError = { it.printStackTrace() } @@ -65,26 +85,27 @@ class GroupedConferencesFragment : RowsSupportFragment() { ) } - private fun render(resource: Resource>) { + private fun render(resource: Resource>) { + // Note: usually we call this after data is loaded. However, for this type of Fragment it does not work properly mainFragmentAdapter.fragmentHost.notifyDataReady(mainFragmentAdapter) - // adapter = ArrayObjectAdapter(ListRowPresenter()) + when (resource) { - is Resource.Success -> (adapter as ArrayObjectAdapter) += resource.data.map { createEventRow(it) } + is Resource.Success -> (adapter as ArrayObjectAdapter) += resource.data is Resource.Error -> Toast.makeText(activity, resource.msg, Toast.LENGTH_LONG).show() } } - private fun createEventRow(conference: ConferenceWithEvents): Row { - val adapter = ArrayObjectAdapter(EventCardPresenter()) - adapter += conference.events - - val headerItem = HeaderItem(conference.conference.title) - return ListRow(headerItem, adapter) - } + // private fun createEventRow(conference: Conference): Row { + // val adapter = ArrayObjectAdapter(EventCardPresenter()) + // adapter += conference + // + // val headerItem = HeaderItem(conference.conference.title) + // return ListRow(headerItem, adapter) + // } companion object { - fun create(conferenceGroup: String): GroupedConferencesFragment { - val fragment = GroupedConferencesFragment() + fun create(conferenceGroup: String): ConferencesFragment { + val fragment = ConferencesFragment() fragment.arguments = Bundle(1).apply { putString(CONFERENCE_GROUP, conferenceGroup) } diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/main/GroupedConferencesViewModel.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/main/ConferencesViewModel.kt similarity index 70% rename from app/src/main/java/de/stefanmedack/ccctv/ui/main/GroupedConferencesViewModel.kt rename to app/src/main/java/de/stefanmedack/ccctv/ui/main/ConferencesViewModel.kt index 84839eb..1510728 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/main/GroupedConferencesViewModel.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/main/ConferencesViewModel.kt @@ -2,12 +2,12 @@ package de.stefanmedack.ccctv.ui.main import android.arch.lifecycle.ViewModel import de.stefanmedack.ccctv.model.Resource -import de.stefanmedack.ccctv.persistence.entities.ConferenceWithEvents +import de.stefanmedack.ccctv.persistence.entities.Conference import de.stefanmedack.ccctv.repository.ConferenceRepository import io.reactivex.Flowable import javax.inject.Inject -class GroupedConferencesViewModel @Inject constructor( +class ConferencesViewModel @Inject constructor( private val repository: ConferenceRepository ) : ViewModel() { @@ -17,11 +17,11 @@ class GroupedConferencesViewModel @Inject constructor( this.conferenceGroup = conferenceGroup } - val conferencesWithEvents: Flowable>> + val conferences: Flowable>> get() = repository.loadedConferences(conferenceGroup) - .map>> { + .map>> { if (it is Resource.Success) - Resource.Success(it.data.sortedByDescending { it.conference.title }) + Resource.Success(it.data.sortedByDescending { it.title }) else it } diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/main/LiveStreamingViewModel.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/main/LiveStreamingViewModel.kt index 426ae63..582e805 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/main/LiveStreamingViewModel.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/main/LiveStreamingViewModel.kt @@ -21,7 +21,7 @@ class LiveStreamingViewModel @Inject constructor( private fun extractConference(): List = streamingRepository.cachedStreams.find { it.conference == conferenceName }?. - groups?.find { it.group == "Live" }?.rooms ?: listOf() + groups?.find { it.group == "Lecture Rooms" }?.rooms ?: listOf() // TODO do not check this change in!!! // val conferencesWithEvents: Flowable>> // get() = repository.loadedConferences(conferenceName) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainFragment.kt index e12d1d9..239ebd9 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainFragment.kt @@ -119,7 +119,7 @@ class MainFragment : BrowseSupportFragment() { return when ((rowObj as Row).headerItem.id) { 6L -> AboutFragment() 2L -> LiveStreamingFragment.create(rowObj.headerItem.name) - else -> GroupedConferencesFragment.create(rowObj.headerItem.name) + else -> ConferencesFragment.create(rowObj.headerItem.name) } } } diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainModule.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainModule.kt index f72b112..3ef878f 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainModule.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainModule.kt @@ -26,7 +26,7 @@ abstract class MainModule { abstract fun contributeMainFragment(): MainFragment @ContributesAndroidInjector - abstract fun contributeConferenceGroupDetailFragment(): GroupedConferencesFragment + abstract fun contributeConferenceGroupDetailFragment(): ConferencesFragment @ContributesAndroidInjector abstract fun contributeLiveStreamingFragment(): LiveStreamingFragment diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainViewModel.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainViewModel.kt index d7000ef..82e3cff 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainViewModel.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/main/MainViewModel.kt @@ -33,13 +33,12 @@ class MainViewModel @Inject constructor( } ) - val conferences: Flowable>> - get() = conferenceRepository.conferencesWithEvents + private val conferences: Flowable>> + get() = conferenceRepository.conferences .map>> { when (it) { // TODO create helper method for Success-Mapping is Resource.Success -> Resource.Success(it.data - .map { it.conference } .groupConferences() .keys .toList()) @@ -49,6 +48,5 @@ class MainViewModel @Inject constructor( } private val streams - get() = - streamingRepository.streams + get() = streamingRepository.streams } \ No newline at end of file diff --git a/app/src/main/res/layout/grid_fragment.xml b/app/src/main/res/layout/grid_fragment.xml new file mode 100644 index 0000000..4e67908 --- /dev/null +++ b/app/src/main/res/layout/grid_fragment.xml @@ -0,0 +1,21 @@ + + + + + + + + + From 769841938e0069e85ba36d355ffe8c0a18787ef3 Mon Sep 17 00:00:00 2001 From: Stefan Medack Date: Mon, 1 Jan 2018 20:19:24 +0100 Subject: [PATCH 08/16] changes conference card style --- .../ccctv/ui/cards/ConferenceCardPresenter.kt | 4 ++-- app/src/main/res/values/dimens.xml | 9 +++++++++ app/src/main/res/values/styles.xml | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/cards/ConferenceCardPresenter.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/cards/ConferenceCardPresenter.kt index 5ecad2d..930c322 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/cards/ConferenceCardPresenter.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/cards/ConferenceCardPresenter.kt @@ -20,7 +20,7 @@ class ConferenceCardPresenter : Presenter() { selectedBackgroundColor = ContextCompat.getColor(parent.context, R.color.amber_800) // TODO ConferenceCardStyle? - val cardView = object : ImageCardView(ContextThemeWrapper(parent.context, R.style.EventCardStyle)) { + val cardView = object : ImageCardView(ContextThemeWrapper(parent.context, R.style.GridCardTheme)) { override fun setSelected(selected: Boolean) { updateCardBackgroundColor(this, selected) super.setSelected(selected) @@ -40,7 +40,7 @@ class ConferenceCardPresenter : Presenter() { it.contentText = item.acronym Glide.with(viewHolder.view.context) .load(item.logoUrl) - .centerCrop() + .fitCenter() // TODO check why fitCenter does not work .error(R.drawable.voctocat) .into(it.mainImageView) } diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 21607c3..fb5615e 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,5 +1,14 @@ + + 200dp + + + + 150dp + + + 300dp 170dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5e28fcb..ebcb1f2 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -26,6 +26,20 @@ 2 + + + + + + + + From 58c5b994f72a8641f8bbb00f729ac997b36311c0 Mon Sep 17 00:00:00 2001 From: Stefan Medack Date: Tue, 2 Jan 2018 21:23:23 +0100 Subject: [PATCH 12/16] adds title and background image to EventsFragment --- .../ccctv/ui/events/EventsActivity.kt | 8 ++- .../ccctv/ui/events/EventsFragment.kt | 49 +++++++++++++++++++ .../de/stefanmedack/ccctv/util/Constants.kt | 2 + 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/events/EventsActivity.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/events/EventsActivity.kt index 89d9503..7423c4f 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/events/EventsActivity.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/events/EventsActivity.kt @@ -8,6 +8,8 @@ import de.stefanmedack.ccctv.R import de.stefanmedack.ccctv.persistence.entities.Conference import de.stefanmedack.ccctv.ui.base.BaseInjectableActivity import de.stefanmedack.ccctv.util.CONFERENCE_ID +import de.stefanmedack.ccctv.util.CONFERENCE_LOGO_URL +import de.stefanmedack.ccctv.util.CONFERENCE_TITLE import de.stefanmedack.ccctv.util.replaceFragmentInTransaction class EventsActivity : BaseInjectableActivity() { @@ -22,8 +24,10 @@ class EventsActivity : BaseInjectableActivity() { fragment = EventsFragment() fragment?.let { frag -> - frag.arguments = Bundle(1).apply { + frag.arguments = Bundle(3).apply { putInt(CONFERENCE_ID, intent.getIntExtra(CONFERENCE_ID, -1)) + putString(CONFERENCE_TITLE, intent.getStringExtra(CONFERENCE_TITLE)) + putString(CONFERENCE_LOGO_URL, intent.getStringExtra(CONFERENCE_LOGO_URL)) } replaceFragmentInTransaction(frag, R.id.fragment, EVENTS_TAG) } @@ -33,6 +37,8 @@ class EventsActivity : BaseInjectableActivity() { fun start(activity: Activity, conference: Conference) { val intent = Intent(activity.baseContext, EventsActivity::class.java) intent.putExtra(CONFERENCE_ID, conference.id) + intent.putExtra(CONFERENCE_TITLE, conference.title) + intent.putExtra(CONFERENCE_LOGO_URL, conference.logoUrl) activity.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation(activity).toBundle()) } diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/events/EventsFragment.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/events/EventsFragment.kt index c9a8b94..9ff35c6 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/events/EventsFragment.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/events/EventsFragment.kt @@ -2,11 +2,20 @@ package de.stefanmedack.ccctv.ui.events import android.arch.lifecycle.ViewModelProvider import android.arch.lifecycle.ViewModelProviders +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.Paint import android.os.Bundle +import android.support.v17.leanback.app.BackgroundManager import android.support.v17.leanback.app.VerticalGridSupportFragment import android.support.v17.leanback.widget.* +import android.util.DisplayMetrics import android.view.View import android.widget.Toast +import com.bumptech.glide.Glide +import com.bumptech.glide.request.animation.GlideAnimation +import com.bumptech.glide.request.target.SimpleTarget import dagger.android.support.AndroidSupportInjection import de.stefanmedack.ccctv.model.Resource import de.stefanmedack.ccctv.persistence.entities.ConferenceWithEvents @@ -15,6 +24,8 @@ import de.stefanmedack.ccctv.ui.cards.EventCardPresenter import de.stefanmedack.ccctv.ui.detail.DetailActivity import de.stefanmedack.ccctv.util.CONFERENCE_GROUP import de.stefanmedack.ccctv.util.CONFERENCE_ID +import de.stefanmedack.ccctv.util.CONFERENCE_LOGO_URL +import de.stefanmedack.ccctv.util.CONFERENCE_TITLE import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.subscribeBy import javax.inject.Inject @@ -40,6 +51,11 @@ class EventsFragment : VerticalGridSupportFragment() { setupUi() } + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + prepareBackground() + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { AndroidSupportInjection.inject(this) super.onViewCreated(view, savedInstanceState) @@ -53,6 +69,8 @@ class EventsFragment : VerticalGridSupportFragment() { } private fun setupUi() { + title = arguments?.getString(CONFERENCE_TITLE) ?: "" + showTitle(true) gridPresenter = VerticalGridPresenter(ZOOM_FACTOR).apply { numberOfColumns = COLUMNS @@ -71,6 +89,36 @@ class EventsFragment : VerticalGridSupportFragment() { prepareEntranceTransition() } + private fun prepareBackground() { + activity?.let { activityContext -> + val backgroundManager = BackgroundManager.getInstance(activityContext) + backgroundManager.attach(activityContext.window) + + val metrics = DisplayMetrics() + activityContext.windowManager.defaultDisplay.getMetrics(metrics) + val width = metrics.widthPixels + val height = metrics.heightPixels + + Glide.with(activityContext) + .load(arguments?.getString(CONFERENCE_LOGO_URL)) + .asBitmap() + .override(width, height) + .fitCenter() + .into>(object : SimpleTarget(width, height) { + override fun onResourceReady(resource: Bitmap, glideAnimation: GlideAnimation) { + backgroundManager?.setBitmap(darkenBitMap(resource)) + } + }) + } + } + + private fun darkenBitMap(bm: Bitmap): Bitmap { + val canvas = Canvas(bm) + canvas.drawARGB(200, 0, 0, 0) + canvas.drawBitmap(bm, Matrix(), Paint()) + return bm + } + private fun bindViewModel() { disposables.add(viewModel.conferenceWithEvents .subscribeBy( @@ -85,6 +133,7 @@ class EventsFragment : VerticalGridSupportFragment() { is Resource.Success -> (adapter as ArrayObjectAdapter).addAll(0, resource.data.events) is Resource.Error -> Toast.makeText(activity, resource.msg, Toast.LENGTH_LONG).show() } + // TODO why does the entrance transition not work??? startEntranceTransition() } diff --git a/app/src/main/java/de/stefanmedack/ccctv/util/Constants.kt b/app/src/main/java/de/stefanmedack/ccctv/util/Constants.kt index 298a438..393b816 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/util/Constants.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/util/Constants.kt @@ -22,6 +22,8 @@ const val EVENT_ID = "EventId" const val EVENT_PICTURE = "EventPicture" const val CONFERENCE_GROUP = "CGroup" const val CONFERENCE_ID = "ConferenceId" +const val CONFERENCE_TITLE = "ConferenceTitle" +const val CONFERENCE_LOGO_URL = "ConferenceLogo" const val STREAM_ID = "StreamId" const val STREAM_URL = "StreamUrl" From d8db90228222558e8dff35c2892c20c27a5a259a Mon Sep 17 00:00:00 2001 From: Stefan Medack Date: Wed, 3 Jan 2018 21:41:44 +0100 Subject: [PATCH 13/16] changes DetailView to load an event from remote, if it can not be find in the local db --- .../ccctv/persistence/daos/EventDao.kt | 3 +- .../ccctv/persistence/entities/Event.kt | 14 ++--- .../ccctv/repository/EventRepository.kt | 7 ++- .../ccctv/ui/detail/DetailViewModel.kt | 33 ++++++---- .../detail/playback/BaseExoPlayerAdapter.kt | 2 +- .../repository/ConferenceRepositoryTest.kt | 62 ++++++++++++------- .../ccctv/repository/EventRepositoryTest.kt | 61 ++++++++++++++++++ 7 files changed, 135 insertions(+), 47 deletions(-) create mode 100644 app/src/test/java/de/stefanmedack/ccctv/repository/EventRepositoryTest.kt diff --git a/app/src/main/java/de/stefanmedack/ccctv/persistence/daos/EventDao.kt b/app/src/main/java/de/stefanmedack/ccctv/persistence/daos/EventDao.kt index 7d39bcc..eacdb83 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/persistence/daos/EventDao.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/persistence/daos/EventDao.kt @@ -6,6 +6,7 @@ import android.arch.persistence.room.OnConflictStrategy import android.arch.persistence.room.Query import de.stefanmedack.ccctv.persistence.entities.Event import io.reactivex.Flowable +import io.reactivex.Single @Dao interface EventDao { @@ -17,7 +18,7 @@ interface EventDao { fun getEvents(ids: List): Flowable> @Query("SELECT * FROM Events WHERE id = :id") - fun getEventById(id: Int): Flowable + fun getEventById(id: Int): Single @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(event: Event) diff --git a/app/src/main/java/de/stefanmedack/ccctv/persistence/entities/Event.kt b/app/src/main/java/de/stefanmedack/ccctv/persistence/entities/Event.kt index 3b3eb55..e5f6132 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/persistence/entities/Event.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/persistence/entities/Event.kt @@ -11,14 +11,12 @@ import org.threeten.bp.LocalDate import org.threeten.bp.OffsetDateTime @Entity(tableName = "events", - foreignKeys = arrayOf( - ForeignKey( - entity = Conference::class, - parentColumns = arrayOf("id"), - childColumns = arrayOf("conference_id"), - onDelete = CASCADE - )) -) + foreignKeys = [ForeignKey( + entity = Conference::class, + parentColumns = arrayOf("id"), + childColumns = arrayOf("conference_id"), + onDelete = CASCADE + )]) data class Event( @PrimaryKey diff --git a/app/src/main/java/de/stefanmedack/ccctv/repository/EventRepository.kt b/app/src/main/java/de/stefanmedack/ccctv/repository/EventRepository.kt index 8cd084a..cd433ec 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/repository/EventRepository.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/repository/EventRepository.kt @@ -2,6 +2,7 @@ package de.stefanmedack.ccctv.repository import de.stefanmedack.ccctv.persistence.daos.EventDao import de.stefanmedack.ccctv.persistence.entities.Event +import de.stefanmedack.ccctv.persistence.toEntity import de.stefanmedack.ccctv.util.applySchedulers import info.metadude.kotlin.library.c3media.RxC3MediaService import io.reactivex.Flowable @@ -15,7 +16,11 @@ class EventRepository @Inject constructor( private val eventDao: EventDao ) { fun getEvent(id: Int): Flowable = eventDao.getEventById(id) - .applySchedulers() + .onErrorResumeNext( + mediaService.getEvent(id) + .applySchedulers() + .map { it.toEntity(-1)!! } + ).toFlowable() fun getEvents(ids: List): Flowable> = eventDao.getEvents(ids) .applySchedulers() diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailViewModel.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailViewModel.kt index 35a4c64..6d7e728 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailViewModel.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/DetailViewModel.kt @@ -21,19 +21,26 @@ class DetailViewModel @Inject constructor( } val detailUi: Flowable - get() = repository.getEvent(eventId) - .flatMap { event: Event -> - // TODO fix parsing of related events - getRelatedEvents(/*detailUiModel.event.metadata?.related ?: */listOf()) - .map { - DetailUiModel( - event = event, - speaker = event.persons.map { SpeakerUiModel(it) }, - related = it - ) - } - } - .share() + get() = + repository.getEvent(eventId).map { event -> + DetailUiModel( + event = event, + speaker = event.persons.map { SpeakerUiModel(it) }, + related = listOf() + ) + } + // TODO fix parsing of related events + // repository.getEvent(eventId) + // .flatMap { event: Event -> + // getRelatedEvents(/*detailUiModel.event.metadata?.related ?: */listOf()) + // .map { + // DetailUiModel( + // event = event, + // speaker = event.persons.map { SpeakerUiModel(it) }, + // related = it + // ) + // } + // } private fun getRelatedEvents(relatedIds: List): Flowable> = repository .getEvents(relatedIds) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/BaseExoPlayerAdapter.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/BaseExoPlayerAdapter.kt index 5bcc5b6..4a83751 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/BaseExoPlayerAdapter.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/detail/playback/BaseExoPlayerAdapter.kt @@ -38,7 +38,7 @@ open class BaseExoPlayerAdapter(private val context: Context) : PlayerAdapter(), handler.postDelayed(this, updatePeriod) } } - internal val handler = Handler() + internal val handler by lazy { Handler() } internal var initialized = false internal var hasDisplay: Boolean = false internal var bufferingStart: Boolean = false diff --git a/app/src/test/java/de/stefanmedack/ccctv/repository/ConferenceRepositoryTest.kt b/app/src/test/java/de/stefanmedack/ccctv/repository/ConferenceRepositoryTest.kt index b6a70da..c2ac8fd 100644 --- a/app/src/test/java/de/stefanmedack/ccctv/repository/ConferenceRepositoryTest.kt +++ b/app/src/test/java/de/stefanmedack/ccctv/repository/ConferenceRepositoryTest.kt @@ -1,10 +1,21 @@ package de.stefanmedack.ccctv.repository +import de.stefanmedack.ccctv.minimalConferenceEntity +import de.stefanmedack.ccctv.model.Resource import de.stefanmedack.ccctv.persistence.daos.ConferenceDao +import de.stefanmedack.ccctv.persistence.daos.EventDao +import de.stefanmedack.ccctv.persistence.preferences.C3SharedPreferences import info.metadude.kotlin.library.c3media.RxC3MediaService +import info.metadude.kotlin.library.c3media.models.ConferencesResponse +import io.reactivex.Flowable +import io.reactivex.Single import io.reactivex.android.plugins.RxAndroidPlugins import io.reactivex.schedulers.Schedulers +import org.amshove.kluent.When +import org.amshove.kluent.calling +import org.amshove.kluent.itReturns import org.junit.Before +import org.junit.Test import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.MockitoAnnotations @@ -18,6 +29,12 @@ class ConferenceRepositoryTest { @Mock internal lateinit var conferenceDao: ConferenceDao + @Mock + internal lateinit var eventDao: EventDao + + @Mock + internal lateinit var preferences: C3SharedPreferences + @InjectMocks internal lateinit var repositoy: ConferenceRepository @@ -27,29 +44,28 @@ class ConferenceRepositoryTest { RxAndroidPlugins.setInitMainThreadSchedulerHandler { _ -> Schedulers.trampoline() } } - // TODO add back in case plain conferences are still needed -// @Test -// fun `fetch conferences from local`() { -// val confList = listOf(minimalConferenceEntity) -// When calling conferenceDao.getConferences() itReturns Flowable.just(confList) -// When calling mediaService.getConferences() itReturns Single.never() -// -// val result = repositoy.conferences.test().await() -// -// result.assertValueAt(0, Resource.Loading()) -// result.assertValueAt(1, Resource.Success(confList)) -// } -// -// @Test -// fun `fetch conferences from network`() { -// When calling conferenceDao.getConferences() itReturns Flowable.empty() -// When calling mediaService.getConferences() itReturns Single.just(ConferencesResponse(listOf())) -// -// val result = repositoy.conferences.test().await() -// -// result.assertValueAt(0, Resource.Loading()) -// result.assertValueAt(1, Resource.Success(listOf())) -// } + @Test + fun `fetch conferences from local`() { + val confList = listOf(minimalConferenceEntity) + When calling conferenceDao.getConferences() itReturns Flowable.just(confList) + When calling mediaService.getConferences() itReturns Single.never() + + val result = repositoy.conferences.test().await() + + result.assertValueAt(0, Resource.Loading()) + result.assertValueAt(1, Resource.Success(confList)) + } + + @Test + fun `fetch conferences from network`() { + When calling conferenceDao.getConferences() itReturns Flowable.empty() + When calling mediaService.getConferences() itReturns Single.just(ConferencesResponse(listOf())) + + val result = repositoy.conferences.test().await() + + result.assertValueAt(0, Resource.Loading()) + result.assertValueAt(1, Resource.Success(listOf())) + } // TODO implement conference with events tests // @Test diff --git a/app/src/test/java/de/stefanmedack/ccctv/repository/EventRepositoryTest.kt b/app/src/test/java/de/stefanmedack/ccctv/repository/EventRepositoryTest.kt new file mode 100644 index 0000000..448fc08 --- /dev/null +++ b/app/src/test/java/de/stefanmedack/ccctv/repository/EventRepositoryTest.kt @@ -0,0 +1,61 @@ +package de.stefanmedack.ccctv.repository + +import de.stefanmedack.ccctv.minimalEvent +import de.stefanmedack.ccctv.minimalEventEntity +import de.stefanmedack.ccctv.persistence.daos.EventDao +import de.stefanmedack.ccctv.persistence.toEntity +import info.metadude.kotlin.library.c3media.RxC3MediaService +import io.reactivex.Single +import io.reactivex.android.plugins.RxAndroidPlugins +import io.reactivex.schedulers.Schedulers +import org.amshove.kluent.When +import org.amshove.kluent.calling +import org.amshove.kluent.itReturns +import org.junit.Before +import org.junit.Test +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("IllegalIdentifier") +class EventRepositoryTest { + + @Mock + internal lateinit var mediaService: RxC3MediaService + + @Mock + internal lateinit var eventDao: EventDao + + @InjectMocks + internal lateinit var repositoy: EventRepository + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + RxAndroidPlugins.setInitMainThreadSchedulerHandler { _ -> Schedulers.trampoline() } + } + + @Test + fun `fetch event by id from local source`() { + val id = minimalEventEntity.id + + When calling eventDao.getEventById(id) itReturns Single.just(minimalEventEntity) + When calling mediaService.getEvent(id) itReturns Single.error(Exception("MediaService throw an error")) + + val result = repositoy.getEvent(id).test().await() + result.assertValueAt(0, minimalEventEntity) + } + + @Test + fun `fetch event by id from remote when local throws an error`() { + val id = 8 + val eventRemote = minimalEvent.copy(url = "https://api.media.ccc.de/public/events/8") + + When calling eventDao.getEventById(id) itReturns Single.error(Exception("EventDao throw an error")) + When calling mediaService.getEvent(id) itReturns Single.just(eventRemote) + + val result = repositoy.getEvent(id).test().await() + result.assertValueAt(0, eventRemote.toEntity(-1)) + } + +} \ No newline at end of file From 0f587c699e735b9218f288be9d96e7c62b661ae3 Mon Sep 17 00:00:00 2001 From: Stefan Medack Date: Wed, 3 Jan 2018 22:22:33 +0100 Subject: [PATCH 14/16] resolves remaining TODOs --- .../ccctv/ui/cards/ConferenceCardPresenter.kt | 1 - .../ccctv/ui/main/LiveStreamingViewModel.kt | 10 +++++++--- app/src/main/res/values/dimens.xml | 5 ----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/cards/ConferenceCardPresenter.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/cards/ConferenceCardPresenter.kt index 930c322..e829cb8 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/cards/ConferenceCardPresenter.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/cards/ConferenceCardPresenter.kt @@ -19,7 +19,6 @@ class ConferenceCardPresenter : Presenter() { defaultBackgroundColor = ContextCompat.getColor(parent.context, R.color.teal_900) selectedBackgroundColor = ContextCompat.getColor(parent.context, R.color.amber_800) - // TODO ConferenceCardStyle? val cardView = object : ImageCardView(ContextThemeWrapper(parent.context, R.style.GridCardTheme)) { override fun setSelected(selected: Boolean) { updateCardBackgroundColor(this, selected) diff --git a/app/src/main/java/de/stefanmedack/ccctv/ui/main/LiveStreamingViewModel.kt b/app/src/main/java/de/stefanmedack/ccctv/ui/main/LiveStreamingViewModel.kt index 582e805..73a1e02 100644 --- a/app/src/main/java/de/stefanmedack/ccctv/ui/main/LiveStreamingViewModel.kt +++ b/app/src/main/java/de/stefanmedack/ccctv/ui/main/LiveStreamingViewModel.kt @@ -1,5 +1,6 @@ package de.stefanmedack.ccctv.ui.main +import android.arch.lifecycle.BuildConfig import android.arch.lifecycle.ViewModel import de.stefanmedack.ccctv.repository.StreamingRepository import info.metadude.java.library.brockman.models.Room @@ -19,9 +20,12 @@ class LiveStreamingViewModel @Inject constructor( val roomsForConference: Flowable> get() = Flowable.just(extractConference()) - private fun extractConference(): List - = streamingRepository.cachedStreams.find { it.conference == conferenceName }?. - groups?.find { it.group == "Lecture Rooms" }?.rooms ?: listOf() // TODO do not check this change in!!! + @Suppress("ConstantConditionIf") + private fun extractConference(): List = streamingRepository.cachedStreams + .find { it.conference == conferenceName } + ?.groups + ?.find { it.group == if (BuildConfig.DEBUG) "Lecture Rooms" else "Live" } + ?.rooms ?: listOf() // val conferencesWithEvents: Flowable>> // get() = repository.loadedConferences(conferenceName) diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 657e9d4..30ece54 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -2,12 +2,7 @@ 200dp - - - 150dp - - 200dp 112dp From 6262eda0c2e5be31e7cbd52a66a80264ff8f136b Mon Sep 17 00:00:00 2001 From: Stefan Medack Date: Wed, 3 Jan 2018 22:38:02 +0100 Subject: [PATCH 15/16] display more information on event cards --- app/src/main/res/values/styles.xml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 1fffd15..1c30d75 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -17,7 +17,7 @@ - + + @@ -47,7 +52,8 @@