remove RxJava and move Downloader from touch-project

This commit is contained in:
Felix 2018-09-06 19:08:08 +02:00
parent 14609cc907
commit c2a83fae2d
14 changed files with 376 additions and 41 deletions

View file

@ -45,12 +45,9 @@ dependencies {
api "android.arch.persistence.room:rxjava2:${rootProject.ext.archCompVersion}"
kapt "android.arch.persistence.room:compiler:${rootProject.ext.archCompVersion}"
api 'io.reactivex.rxjava2:rxjava:2.1.5'
api 'com.squareup.retrofit2:retrofit:2.3.0'
api 'com.squareup.retrofit2:converter-gson:2.3.0'
api 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
api 'com.fasterxml.jackson.module:jackson-module-kotlin:2.9.0'

View file

@ -27,12 +27,14 @@ data class Recording(
) {
var recordingID: Long
var eventID: Long
init {
val strings = url.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
recordingID = (strings[strings.size - 1]).toLong()
}
fun getEventString():String {
val split = eventUrl.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
eventID = (split[split.size - 1]).toLong()
return (split[split.size - 1])
}
}

View file

@ -27,7 +27,7 @@ interface EventDao: PersistentItemDao<PersistentEvent> {
@Query("SELECT * FROM event WHERE conference = :id ORDER BY title ASC")
fun findEventsByConference(id: Long):LiveData<List<PersistentEvent>>
@Query("SELECT * FROM event WHERE conference = :id ORDER BY title ASC")
@Query("SELECT * FROM event WHERE conferenceId = :id ORDER BY title ASC")
fun findEventsByConferenceSync(id: Long):List<PersistentEvent>
@Query("SELECT * FROM event INNER JOIN watchlist_item WHERE event.id = watchlist_item.event_id")

View file

@ -13,7 +13,7 @@ import de.nicidienase.chaosflix.common.mediadata.entities.recording.Recording
childColumns = arrayOf("eventId"))),
indices = arrayOf(Index("eventId")))
data class PersistentRecording(
var eventId: Long,
var eventId: Long = 0,
var size: Int = 0,
var length: Int = 0,
var mimeType: String = "",
@ -51,23 +51,23 @@ data class PersistentRecording(
}
@Ignore
constructor(rec: Recording) : this(
rec.eventID,
rec.size,
rec.length,
rec.mimeType,
rec.language,
rec.filename,
rec.state,
rec.folder,
rec.isHighQuality,
rec.width,
rec.height,
rec.updatedAt,
rec.recordingUrl,
rec.url,
rec.eventUrl,
rec.conferenceUrl)
constructor(rec: Recording, eventId: Long = 0) : this(
eventId = eventId,
size = rec.size,
length = rec.length,
mimeType = rec.mimeType,
language = rec.language,
filename = rec.filename,
state = rec.state,
folder = rec.folder,
isHighQuality = rec.isHighQuality,
width = rec.width,
height = rec.height,
updatedAt = rec.updatedAt,
recordingUrl = rec.recordingUrl,
url = rec.url,
eventUrl = rec.eventUrl,
conferenceUrl = rec.conferenceUrl)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeLong(eventId)

View file

@ -7,7 +7,7 @@ import android.arch.persistence.room.OnConflictStrategy
import android.arch.persistence.room.Query
@Dao
interface RecordingDao{
interface RecordingDao: PersistentItemDao<RecordingDao>{
@Query("SELECT * FROM recording")
fun getAllRecordings(): LiveData<List<PersistentRecording>>

View file

@ -0,0 +1,37 @@
package de.nicidienase.chaosflix.common.mediadata.network
import android.content.res.Resources
import com.google.gson.Gson
import de.nicidienase.chaosflix.R
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
class ApiFactory(val res: Resources) {
val gsonConverterFactory by lazy { GsonConverterFactory.create(Gson()) }
val client: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build()
}
val recordingApi: RecordingService by lazy {
Retrofit.Builder()
.baseUrl(res.getString(R.string.api_media_ccc_url))
.client(client)
.addConverterFactory(gsonConverterFactory)
.build()
.create(RecordingService::class.java)
}
val streamingApi: StreamingService by lazy { Retrofit.Builder()
.baseUrl(res.getString(R.string.streaming_media_ccc_url))
.client(client)
.addConverterFactory(gsonConverterFactory)
.build()
.create(StreamingService::class.java) }
}

View file

@ -4,31 +4,28 @@ import de.nicidienase.chaosflix.common.mediadata.entities.recording.Conference
import de.nicidienase.chaosflix.common.mediadata.entities.recording.ConferencesWrapper
import de.nicidienase.chaosflix.common.mediadata.entities.recording.Event
import de.nicidienase.chaosflix.common.mediadata.entities.recording.Recording
import io.reactivex.Single
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
public interface RecordingService {
interface RecordingService {
@GET("public/conferences")
fun getConferencesWrapper(): Single<ConferencesWrapper>
@GET("public/events")
fun getAllEvents(): Single<List<Event>>
fun getConferencesWrapper(): Call<ConferencesWrapper>
@GET("public/conferences/{id}")
fun getConference(@Path("id") id: Long): Single<Conference>
@GET("public/conferences/{id}")
fun getConferenceString(@Path("id") id: Long): Single<String>
fun getConference(@Path("id") id: Long): Call<Conference>
@GET("public/conferences/{name}")
fun getConferenceByname(@Path("name") name: String): Single<Conference>
fun getConferenceByName(@Path("name") name: String): Call<Conference>
@GET("public/events/{id}")
fun getEvent(@Path("id") id: Long): Single<Event>
fun getEvent(@Path("id") id: Long): Call<Event>
@GET("public/events/{guid}")
fun getEventByGUID(@Path("guid") guid: String): Call<Event>
@GET("public/recordings/{id}")
fun getRecording(@Path("id") id: Long): Single<Recording>
fun getRecording(@Path("id") id: Long): Call<Recording>
}
}

View file

@ -1,11 +1,10 @@
package de.nicidienase.chaosflix.common.mediadata.network
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.LiveConference
import io.reactivex.Flowable
import io.reactivex.Single
import retrofit2.http.GET
public interface StreamingService {
interface StreamingService {
@GET("streams/v2.json")
fun getStreamingConferences(): Single<List<LiveConference>>

View file

@ -0,0 +1,32 @@
package de.nicidienase.chaosflix.touch.sync
import android.content.Intent
import android.support.v4.app.JobIntentService
import de.nicidienase.chaosflix.common.mediadata.sync.Downloader
import de.nicidienase.chaosflix.touch.ViewModelFactory
class DownloadJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
val downloader = Downloader(ViewModelFactory.recordingApi, ViewModelFactory.database)
val entity: String? = intent.getStringExtra(ENTITY_KEY)
val id: Long = intent.getLongExtra(ID_KEY, -1)
if (entity != null) {
when (entity) {
// ENTITY_KEY_EVERYTHING -> downloader.updateEverything()
ENTITY_KEY_CONFERENCES -> downloader.updateConferencesAndGroups()
ENTITY_KEY_EVENTS -> downloader.updateEventsForConference(id)
ENTITY_KEY_RECORDINGS -> downloader.updateRecordingsForEvent(id)
}
}
}
companion object {
val ENTITY_KEY: String = "entity_key"
// val ENTITY_KEY_EVERYTHING = "everything"
val ENTITY_KEY_CONFERENCES: String = "conferences"
val ENTITY_KEY_EVENTS: String = "events"
val ENTITY_KEY_RECORDINGS: String = "recodings"
val ID_KEY: String = "id_key"
}
}

View file

@ -0,0 +1,153 @@
package de.nicidienase.chaosflix.common.mediadata.sync
import android.arch.lifecycle.LiveData
import android.database.sqlite.SQLiteConstraintException
import android.util.Log
import de.nicidienase.chaosflix.common.Util
import de.nicidienase.chaosflix.common.mediadata.entities.MediaDatabase
import de.nicidienase.chaosflix.common.mediadata.entities.recording.ConferencesWrapper
import de.nicidienase.chaosflix.common.mediadata.entities.recording.Event
import de.nicidienase.chaosflix.common.mediadata.entities.recording.Recording
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.ConferenceGroup
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentConference
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentRecording
import de.nicidienase.chaosflix.common.mediadata.network.RecordingService
import de.nicidienase.chaosflix.common.util.LiveEvent
import de.nicidienase.chaosflix.common.util.SingleLiveEvent
import de.nicidienase.chaosflix.common.util.ThreadHandler
import io.reactivex.schedulers.Schedulers
class Downloader(val recordingApi: RecordingService,
val database: MediaDatabase) {
private val threadHandler = ThreadHandler()
enum class DownloaderState{
RUNNING, DONE
}
fun updateConferencesAndGroups(): LiveData<LiveEvent<DownloaderState, ConferencesWrapper, String>> {
val updateState = SingleLiveEvent<LiveEvent<DownloaderState, ConferencesWrapper, String>>()
threadHandler.runOnBackgroundThread {
updateState.postValue(LiveEvent(DownloaderState.RUNNING,null, null))
val response = recordingApi.getConferencesWrapper().execute()
if(!response.isSuccessful){
updateState.postValue(LiveEvent(state = DownloaderState.DONE, error = response.message()))
return@runOnBackgroundThread
}
try {
response.body()?.let { conferencesWrapper ->
saveConferences(conferencesWrapper)
updateState.postValue(LiveEvent(DownloaderState.DONE,conferencesWrapper))
}
} catch (e: Exception){
updateState.postValue(LiveEvent(DownloaderState.DONE, error = "Error updating conferences."))
e.printStackTrace()
}
}
return updateState
}
private val TAG: String? = Downloader::class.simpleName
fun updateEventsForConference(conference: PersistentConference) : LiveData<LiveEvent<DownloaderState, List<PersistentEvent>, String>> {
val updateState = SingleLiveEvent<LiveEvent<DownloaderState, List<PersistentEvent>, String>>()
updateState.postValue(LiveEvent(DownloaderState.RUNNING))
threadHandler.runOnBackgroundThread {
val response = recordingApi.getConferenceByName(conference.acronym).execute()
if(!response.isSuccessful){
updateState.postValue(LiveEvent(DownloaderState.DONE,error = response.message()))
return@runOnBackgroundThread
}
try {
val persistentEvents = response.body()?.events?.let { events ->
return@let saveEvents(conference, events)
}
updateState.postValue(LiveEvent(DownloaderState.DONE, data = persistentEvents))
} catch (e: Exception){
updateState.postValue(LiveEvent(DownloaderState.DONE, error = "Error updating Events for ${conference.acronym}"))
e.printStackTrace()
}
}
return updateState
}
fun updateRecordingsForEvent(event: PersistentEvent) :
LiveData<LiveEvent<DownloaderState, List<PersistentRecording>, String>> {
val updateState = SingleLiveEvent<LiveEvent<DownloaderState, List<PersistentRecording>, String>>()
updateState.postValue(LiveEvent(DownloaderState.RUNNING))
threadHandler.runOnBackgroundThread {
val response = recordingApi.getEventByGUID(event.guid).execute()
if(!response.isSuccessful){
updateState.postValue(LiveEvent(DownloaderState.DONE, error = response.message()))
return@runOnBackgroundThread
}
try {
val recordings = response.body()?.recordings?.let { recordings ->
return@let saveRecordings(event, recordings)
}
updateState.postValue(LiveEvent(DownloaderState.DONE, data = recordings))
} catch (e: Exception){
updateState.postValue(LiveEvent(DownloaderState.DONE, error = "Error updating Recordings for ${event.title}"))
e.printStackTrace()}
}
return updateState
}
fun saveConferences(conferencesWrapper: ConferencesWrapper): List<PersistentConference> {
return conferencesWrapper.conferencesMap.map { entry ->
val conferenceGroup: ConferenceGroup = getOrCreateConferenceGroup(entry.key)
val conferenceList = entry.value
.map { PersistentConference(it) }
.map { it.conferenceGroupId = conferenceGroup.id; it }.toTypedArray()
val conferenceIds = database.conferenceDao().insert(*conferenceList).toList()
return@map conferenceList.zip(conferenceIds) { conf: PersistentConference, id: Long ->
conf.id = id
return@zip conf
}
}.flatten()
}
private fun getOrCreateConferenceGroup(name: String): ConferenceGroup{
val conferenceGroup: ConferenceGroup?
= database.conferenceGroupDao().getConferenceGroupByName(name)
if (conferenceGroup != null) {
return conferenceGroup
}
val group = ConferenceGroup(name)
val index = Util.orderedConferencesList.indexOf(group.name)
if (index != -1)
group.index = index
else if (group.name == "other conferences")
group.index = 1_000_001
group.id = database.conferenceGroupDao().insert(group)
return group
}
private fun saveEvents(persistentConference: PersistentConference, events: List<Event>): List<PersistentEvent> {
val persistantEvents = events.map { PersistentEvent(it,persistentConference.id) }
val insertEventIds = database.eventDao().insert(*(persistantEvents.toTypedArray())).asList()
val oldEvents = database.eventDao()
.findEventsByConferenceSync(persistentConference.id)
.filter { !insertEventIds.contains(it.id) }
.toTypedArray()
try {
database.eventDao().delete(*oldEvents)
} catch (ex: SQLiteConstraintException){
Log.d(TAG,"SQLiteException",ex)
}
persistantEvents.zip(insertEventIds) {event: PersistentEvent, id: Long ->
event.id = id
}
return persistantEvents
}
private fun saveRecordings(event: PersistentEvent,recordings: List<Recording>): List<PersistentRecording> {
val persistentRecordings = recordings.map { PersistentRecording(it, event.id) }
database.recordingDao().insert(*persistentRecordings.toTypedArray())
return persistentRecordings
}
}

View file

@ -0,0 +1,6 @@
package de.nicidienase.chaosflix.common.util
class LiveEvent<T,U,V>(
val state: T,
val data: U? = null,
val error: V? = null)

View file

@ -0,0 +1,76 @@
/*
* Copyright 2017 Google Inc.
*
* 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.nicidienase.chaosflix.common.util
import android.arch.lifecycle.LifecycleOwner
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.Observer
import android.support.annotation.MainThread
import android.util.Log
import java.util.concurrent.atomic.AtomicBoolean
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
*
*
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
*
*
* Note that only one observer is going to be notified of changes.
*/
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
@MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
companion object {
private val TAG = "SingleLiveEvent"
}
}

View file

@ -0,0 +1,31 @@
package de.nicidienase.chaosflix.common.util
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.os.Process
class ThreadHandler {
private val uiThreadHandler: Handler
private val backgroundThreadHandler: Handler
init {
val handlerTread = HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND)
handlerTread.start()
backgroundThreadHandler = android.os.Handler(handlerTread.looper)
uiThreadHandler = Handler(Looper.getMainLooper())
}
fun runOnBackgroundThread(runnable: ()->Unit){
backgroundThreadHandler.post(runnable)
}
fun runOnMainThread(runnable: ()->Unit){
uiThreadHandler.post(runnable)
}
companion object {
private val TAG = ThreadHandler::class.java.simpleName
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="recording_url">https://api.media.ccc.de</string>
<string name="streaming_url">https://streaming.media.ccc.de</string>
</resources>