Skip to content

Commit 775590e

Browse files
committed
Merge branch 'feature/live_streams' into develop
2 parents b04d2bf + 90f4058 commit 775590e

23 files changed

+820
-42
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Change Log
22

3+
## 2.0.0
4+
5+
- adds streaming
6+
- adds Room for caching Events
7+
38
## 1.2.0
49

510
### Added

app/build.gradle

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ android {
4141
applicationId "de.stefanmedack.ccctv"
4242
minSdkVersion 21
4343
targetSdkVersion 26
44-
versionCode 2
45-
versionName "1.1.0"
44+
versionCode 4
45+
versionName "2.0.0-RC"
4646
resConfigs "en", "de"
4747
multiDexEnabled true
4848
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@@ -74,6 +74,22 @@ android {
7474
signingConfig signingConfigs.release
7575
}
7676
}
77+
78+
// TODO think about reworking the dimensions/configs for streaming
79+
flavorDimensions "default"
80+
productFlavors {
81+
dev {
82+
buildConfigField "String", "STREAMING_API_BASE_URL", "\"http://gist.githubusercontent.com\""
83+
buildConfigField "String", "STREAMING_API_OFFERS_PATH",
84+
"\"/MaZderMind/a91f242efb2f446a2237d4596896efd6/raw/7ab4e206f19ed4d63a67917fe6e3a15a96218ac9/streams-v2.json\""
85+
// "\"/johnjohndoe/617bbfa2ac36f5148a049548b419e299/raw/7ab4e206f19ed4d63a67917fe6e3a15a96218ac9/streams-v2.json\""
86+
}
87+
live {
88+
buildConfigField "String", "STREAMING_API_BASE_URL", "\"http://streaming.media.ccc.de\""
89+
buildConfigField "String", "STREAMING_API_OFFERS_PATH", "\"/streams/v2.json\""
90+
}
91+
}
92+
7793
sourceSets {
7894
androidTest.java.srcDirs += "src/test-common/java"
7995
test.java.srcDirs += "src/test-common/java"
@@ -105,7 +121,10 @@ dependencies {
105121
kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
106122
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
107123

124+
// c3media library
108125
implementation "info.metadude.kotlin.library.c3media:c3media-rx-java-2:${c3mediaVersion}"
126+
// c3media streaming
127+
implementation "com.github.johnjohndoe:Brockman:1886e90a92029ceba978e3326202383c63ed8efd"
109128

110129
// Exo Player
111130
implementation "com.google.android.exoplayer:exoplayer:$exoplayerVersion"

app/src/main/AndroidManifest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@
5252
android:theme="@style/Theme.AppCompat"
5353
/>
5454

55+
<activity
56+
android:name=".ui.playback.ExoPlayerActivity"
57+
android:exported="true"
58+
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|layoutDirection"
59+
android:launchMode="singleTask"
60+
android:resizeableActivity="true"
61+
android:supportsPictureInPicture="true"/>
62+
5563
<meta-data
5664
android:name="io.fabric.ApiKey"
5765
android:value="2661981927f4d4deaa30951bef2f35d92f832716"

app/src/main/java/de/stefanmedack/ccctv/di/AppComponent.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ import javax.inject.Singleton
1616

1717
@Singleton
1818
@Component(
19-
modules = arrayOf(
20-
AndroidSupportInjectionModule::class,
21-
C3MediaModule::class,
22-
DatabaseModule::class,
23-
SharedPreferencesModule::class,
24-
ActivityBuilderModule::class
25-
)
19+
modules = [
20+
AndroidSupportInjectionModule::class,
21+
C3MediaModule::class,
22+
DatabaseModule::class,
23+
SharedPreferencesModule::class,
24+
ActivityBuilderModule::class
25+
]
2626
)
2727
interface AppComponent : AndroidInjector<C3TVApp> {
2828

app/src/main/java/de/stefanmedack/ccctv/di/modules/C3MediaModule.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package de.stefanmedack.ccctv.di.modules
33
import android.content.Context
44
import dagger.Module
55
import dagger.Provides
6+
import de.stefanmedack.ccctv.BuildConfig
67
import de.stefanmedack.ccctv.di.Scopes.ApplicationContext
78
import de.stefanmedack.ccctv.di.Scopes.CacheDir
89
import de.stefanmedack.ccctv.util.CACHE_MAX_SIZE_HTTP
10+
import info.metadude.java.library.brockman.ApiModule.provideStreamsService
11+
import info.metadude.java.library.brockman.StreamsService
912
import info.metadude.kotlin.library.c3media.RxApiModule.provideRxC3MediaService
1013
import info.metadude.kotlin.library.c3media.RxC3MediaService
1114
import okhttp3.Cache
@@ -19,10 +22,22 @@ class C3MediaModule {
1922

2023
@Provides
2124
@Singleton
22-
fun provideC3MediaService(@CacheDir cacheDir: File?): RxC3MediaService {
25+
fun provideC3MediaService(okHttpClient: OkHttpClient): RxC3MediaService {
26+
return provideRxC3MediaService("https://api.media.ccc.de", okHttpClient)
27+
}
28+
29+
@Provides
30+
@Singleton
31+
fun provideStreamsService(okHttpClient: OkHttpClient): StreamsService {
32+
return provideStreamsService(BuildConfig.STREAMING_API_BASE_URL, okHttpClient);
33+
}
34+
35+
@Provides
36+
@Singleton
37+
fun provideHttpCient(@CacheDir cacheDir: File?): OkHttpClient {
2338
val interceptor = HttpLoggingInterceptor()
2439
interceptor.level = HttpLoggingInterceptor.Level.HEADERS
25-
val okHttpClient = OkHttpClient.Builder().run {
40+
return OkHttpClient.Builder().run {
2641
addNetworkInterceptor(interceptor)
2742
if (null != cacheDir) {
2843
val responseCache = File(cacheDir, "HttpResponseCache")
@@ -33,7 +48,6 @@ class C3MediaModule {
3348
}
3449
build()
3550
}
36-
return provideRxC3MediaService("https://api.media.ccc.de", okHttpClient)
3751
}
3852

3953
@Provides

app/src/main/java/de/stefanmedack/ccctv/di/modules/ViewModelModule.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import de.stefanmedack.ccctv.di.C3ViewModelFactory
99
import de.stefanmedack.ccctv.di.Scopes.ViewModelKey
1010
import de.stefanmedack.ccctv.ui.detail.DetailViewModel
1111
import de.stefanmedack.ccctv.ui.main.GroupedConferencesViewModel
12+
import de.stefanmedack.ccctv.ui.main.LiveStreamingViewModel
1213
import de.stefanmedack.ccctv.ui.main.MainViewModel
1314
import de.stefanmedack.ccctv.ui.search.SearchViewModel
1415

@@ -25,6 +26,11 @@ abstract class ViewModelModule {
2526
@ViewModelKey(GroupedConferencesViewModel::class)
2627
abstract fun bindGroupedConferencesViewModel(viewModel: GroupedConferencesViewModel): ViewModel
2728

29+
@Binds
30+
@IntoMap
31+
@ViewModelKey(LiveStreamingViewModel::class)
32+
abstract fun bindLiveStreamingViewModel(viewModel: LiveStreamingViewModel): ViewModel
33+
2834
@Binds
2935
@IntoMap
3036
@ViewModelKey(DetailViewModel::class)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package de.stefanmedack.ccctv.repository
2+
3+
import de.stefanmedack.ccctv.BuildConfig
4+
import de.stefanmedack.ccctv.model.Resource
5+
import de.stefanmedack.ccctv.model.Resource.Success
6+
import de.stefanmedack.ccctv.util.createFlowable
7+
import info.metadude.java.library.brockman.StreamsService
8+
import info.metadude.java.library.brockman.models.Offer
9+
import io.reactivex.BackpressureStrategy
10+
import io.reactivex.Flowable
11+
import retrofit2.Call
12+
import retrofit2.Callback
13+
import retrofit2.Response
14+
import javax.inject.Inject
15+
import javax.inject.Singleton
16+
17+
@Singleton
18+
class StreamingRepository @Inject constructor(
19+
private val streamsService: StreamsService
20+
) {
21+
var cachedStreams: List<Offer> = listOf()
22+
23+
val streams: Flowable<Resource<List<Offer>>>
24+
get() =
25+
createFlowable(BackpressureStrategy.LATEST) { emitter ->
26+
emitter.onNext(Resource.Loading())
27+
streamsService.getOffers(BuildConfig.STREAMING_API_OFFERS_PATH).enqueue(object : Callback<List<Offer>> {
28+
override fun onResponse(call: Call<List<Offer>>?, response: Response<List<Offer>>?) {
29+
response?.body()?.let {
30+
cachedStreams = it
31+
emitter.onNext(Success(it))
32+
}
33+
}
34+
35+
override fun onFailure(call: Call<List<Offer>>?, t: Throwable?) {
36+
emitter.onNext(Resource.Error("Could not load streams"))
37+
}
38+
})
39+
}
40+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package de.stefanmedack.ccctv.ui.cards
2+
3+
import android.support.v17.leanback.widget.ImageCardView
4+
import android.support.v17.leanback.widget.Presenter
5+
import android.support.v4.content.ContextCompat
6+
import android.support.v7.view.ContextThemeWrapper
7+
import android.view.ViewGroup
8+
import com.bumptech.glide.Glide
9+
import de.stefanmedack.ccctv.R
10+
import info.metadude.java.library.brockman.models.Stream
11+
import kotlin.properties.Delegates
12+
13+
class StreamCardPresenter(val thumbPictureUrl: String) : Presenter() {
14+
15+
private var selectedBackgroundColor: Int by Delegates.notNull()
16+
private var defaultBackgroundColor: Int by Delegates.notNull()
17+
18+
override fun onCreateViewHolder(parent: ViewGroup): Presenter.ViewHolder {
19+
defaultBackgroundColor = ContextCompat.getColor(parent.context, R.color.teal_900)
20+
selectedBackgroundColor = ContextCompat.getColor(parent.context, R.color.amber_800)
21+
22+
val cardView = object : ImageCardView(ContextThemeWrapper(parent.context, R.style.EventCardStyle)) {
23+
override fun setSelected(selected: Boolean) {
24+
updateCardBackgroundColor(this, selected)
25+
super.setSelected(selected)
26+
}
27+
}
28+
29+
cardView.isFocusable = true
30+
cardView.isFocusableInTouchMode = true
31+
updateCardBackgroundColor(cardView, false)
32+
return Presenter.ViewHolder(cardView)
33+
}
34+
35+
override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, item: Any) {
36+
if (item is Stream) {
37+
(viewHolder.view as ImageCardView).let {
38+
it.titleText = item.display
39+
it.contentText = item.slug
40+
Glide.with(viewHolder.view.context)
41+
.load(thumbPictureUrl)
42+
.centerCrop()
43+
.error(R.drawable.voctocat)
44+
.into(it.mainImageView)
45+
}
46+
}
47+
}
48+
49+
override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) {
50+
(viewHolder.view as ImageCardView).let {
51+
it.badgeImage = null
52+
it.mainImage = null
53+
}
54+
}
55+
56+
private fun updateCardBackgroundColor(view: ImageCardView, selected: Boolean) {
57+
val color = if (selected) selectedBackgroundColor else defaultBackgroundColor
58+
// both background colors should be set because the view's background is temporarily visible during animations.
59+
view.setBackgroundColor(color)
60+
view.setInfoAreaBackgroundColor(color)
61+
}
62+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package de.stefanmedack.ccctv.ui.main
2+
3+
import android.arch.lifecycle.ViewModelProvider
4+
import android.arch.lifecycle.ViewModelProviders
5+
import android.os.Bundle
6+
import android.support.v17.leanback.app.RowsSupportFragment
7+
import android.support.v17.leanback.widget.*
8+
import android.view.View
9+
import dagger.android.support.AndroidSupportInjection
10+
import de.stefanmedack.ccctv.ui.cards.StreamCardPresenter
11+
import de.stefanmedack.ccctv.ui.playback.ExoPlayerActivity
12+
import de.stefanmedack.ccctv.util.STREAM_ID
13+
import de.stefanmedack.ccctv.util.plusAssign
14+
import info.metadude.java.library.brockman.models.Room
15+
import info.metadude.java.library.brockman.models.Stream
16+
import io.reactivex.disposables.CompositeDisposable
17+
import io.reactivex.rxkotlin.subscribeBy
18+
import javax.inject.Inject
19+
20+
class LiveStreamingFragment : RowsSupportFragment() {
21+
22+
@Inject
23+
lateinit var viewModelFactory: ViewModelProvider.Factory
24+
25+
private val viewModel: LiveStreamingViewModel by lazy {
26+
ViewModelProviders.of(activity, viewModelFactory).get(LiveStreamingViewModel::class.java).apply {
27+
init(arguments.getString(STREAM_ID, ""))
28+
}
29+
}
30+
31+
private val disposables = CompositeDisposable()
32+
33+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
34+
AndroidSupportInjection.inject(this)
35+
super.onViewCreated(view, savedInstanceState)
36+
37+
setupUi()
38+
bindViewModel()
39+
}
40+
41+
override fun onDestroy() {
42+
disposables.clear()
43+
super.onDestroy()
44+
}
45+
46+
private fun setupUi() {
47+
adapter = ArrayObjectAdapter(ListRowPresenter())
48+
onItemViewClickedListener = OnItemViewClickedListener { _, item, _, _ ->
49+
if (item is Stream) {
50+
ExoPlayerActivity.start(activity, item)
51+
}
52+
}
53+
}
54+
55+
private fun bindViewModel() {
56+
disposables.add(viewModel.roomsForConference
57+
.subscribeBy(
58+
onNext = { render(it) },
59+
onError = { it.printStackTrace() }
60+
)
61+
)
62+
}
63+
64+
private fun render(rooms: List<Room>) {
65+
mainFragmentAdapter.fragmentHost.notifyDataReady(mainFragmentAdapter)
66+
(adapter as ArrayObjectAdapter) += rooms.map { createEventRow(it) }
67+
}
68+
69+
private fun createEventRow(room: Room): Row {
70+
val adapter = ArrayObjectAdapter(StreamCardPresenter(room.thumb))
71+
adapter += room.streams
72+
73+
val headerItem = HeaderItem(room.scheduleName)
74+
return ListRow(headerItem, adapter)
75+
}
76+
77+
companion object {
78+
fun create(streamId: String): LiveStreamingFragment {
79+
val fragment = LiveStreamingFragment()
80+
fragment.arguments = Bundle(1).apply {
81+
putString(STREAM_ID, streamId)
82+
}
83+
return fragment
84+
}
85+
}
86+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package de.stefanmedack.ccctv.ui.main
2+
3+
import android.arch.lifecycle.ViewModel
4+
import de.stefanmedack.ccctv.repository.StreamingRepository
5+
import info.metadude.java.library.brockman.models.Room
6+
import io.reactivex.Flowable
7+
import javax.inject.Inject
8+
9+
class LiveStreamingViewModel @Inject constructor(
10+
private val streamingRepository: StreamingRepository
11+
) : ViewModel() {
12+
13+
lateinit var conferenceName: String
14+
15+
fun init(streamName: String) {
16+
this.conferenceName = streamName
17+
}
18+
19+
val roomsForConference: Flowable<List<Room>>
20+
get() = Flowable.just(extractConference())
21+
22+
private fun extractConference(): List<Room>
23+
= streamingRepository.cachedStreams.find { it.conference == conferenceName }?.
24+
groups?.find { it.group == "Lecture Rooms" }?.rooms ?: listOf()
25+
26+
// val conferencesWithEvents: Flowable<Resource<List<ConferenceWithEvents>>>
27+
// get() = repository.loadedConferences(conferenceName)
28+
// .map<Resource<List<ConferenceWithEvents>>> {
29+
// if (it is Resource.Success)
30+
// Resource.Success(it.data.sortedByDescending { it.conference.title })
31+
// else
32+
// it
33+
// }
34+
35+
}

0 commit comments

Comments
 (0)