From c2a83fae2d0f3e6d4b9db348dc340e8f4ec08183 Mon Sep 17 00:00:00 2001
From: Felix <mail@nicidienase.de>
Date: Thu, 6 Sep 2018 19:08:08 +0200
Subject: [PATCH] remove RxJava and move Downloader from touch-project

---
 common/build.gradle                           |   3 -
 .../mediadata/entities/recording/Recording.kt |   6 +-
 .../recording/persistence/EventDao.kt         |   2 +-
 .../persistence/PersistentRecording.kt        |  36 ++---
 .../recording/persistence/RecordingDao.kt     |   2 +-
 .../common/mediadata/network/ApiFactory.kt    |  37 +++++
 .../mediadata/network/RecordingService.kt     |  25 ++-
 .../mediadata/network/StreamingService.kt     |   3 +-
 .../mediadata/sync/DownloadJobService.kt      |  32 ++++
 .../common/mediadata/sync/Downloader.kt       | 153 ++++++++++++++++++
 .../chaosflix/common/util/LiveEvent.kt        |   6 +
 .../chaosflix/common/util/SingleLiveEvent.kt  |  76 +++++++++
 .../chaosflix/common/util/ThreadHandler.kt    |  31 ++++
 common/src/main/res/values/urlstrings.xml     |   5 +
 14 files changed, 376 insertions(+), 41 deletions(-)
 create mode 100644 common/src/main/java/de/nicidienase/chaosflix/common/mediadata/network/ApiFactory.kt
 create mode 100644 common/src/main/java/de/nicidienase/chaosflix/common/mediadata/sync/DownloadJobService.kt
 create mode 100644 common/src/main/java/de/nicidienase/chaosflix/common/mediadata/sync/Downloader.kt
 create mode 100644 common/src/main/java/de/nicidienase/chaosflix/common/util/LiveEvent.kt
 create mode 100644 common/src/main/java/de/nicidienase/chaosflix/common/util/SingleLiveEvent.kt
 create mode 100644 common/src/main/java/de/nicidienase/chaosflix/common/util/ThreadHandler.kt
 create mode 100644 common/src/main/res/values/urlstrings.xml

diff --git a/common/build.gradle b/common/build.gradle
index 70ffb96f..b87fd627 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -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'
 
diff --git a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/Recording.kt b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/Recording.kt
index 41ad1bf7..5675861d 100644
--- a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/Recording.kt
+++ b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/Recording.kt
@@ -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])
     }
 }
diff --git a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/persistence/EventDao.kt b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/persistence/EventDao.kt
index 4d506d54..5cb8b331 100644
--- a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/persistence/EventDao.kt
+++ b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/persistence/EventDao.kt
@@ -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")
diff --git a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/persistence/PersistentRecording.kt b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/persistence/PersistentRecording.kt
index 99dcfdfe..bbb99f20 100644
--- a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/persistence/PersistentRecording.kt
+++ b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/persistence/PersistentRecording.kt
@@ -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)
diff --git a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/persistence/RecordingDao.kt b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/persistence/RecordingDao.kt
index 30f75983..10df1f9b 100644
--- a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/persistence/RecordingDao.kt
+++ b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/entities/recording/persistence/RecordingDao.kt
@@ -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>>
diff --git a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/network/ApiFactory.kt b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/network/ApiFactory.kt
new file mode 100644
index 00000000..678cd2d3
--- /dev/null
+++ b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/network/ApiFactory.kt
@@ -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) }
+}
\ No newline at end of file
diff --git a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/network/RecordingService.kt b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/network/RecordingService.kt
index 872defbf..3d56f517 100644
--- a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/network/RecordingService.kt
+++ b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/network/RecordingService.kt
@@ -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>
 
-}
+}
\ No newline at end of file
diff --git a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/network/StreamingService.kt b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/network/StreamingService.kt
index 01a841fc..a08b6268 100644
--- a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/network/StreamingService.kt
+++ b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/network/StreamingService.kt
@@ -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>>
diff --git a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/sync/DownloadJobService.kt b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/sync/DownloadJobService.kt
new file mode 100644
index 00000000..886f1a02
--- /dev/null
+++ b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/sync/DownloadJobService.kt
@@ -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"
+	}
+}
\ No newline at end of file
diff --git a/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/sync/Downloader.kt b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/sync/Downloader.kt
new file mode 100644
index 00000000..f7095a76
--- /dev/null
+++ b/common/src/main/java/de/nicidienase/chaosflix/common/mediadata/sync/Downloader.kt
@@ -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
+	}
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/de/nicidienase/chaosflix/common/util/LiveEvent.kt b/common/src/main/java/de/nicidienase/chaosflix/common/util/LiveEvent.kt
new file mode 100644
index 00000000..9ab23fa1
--- /dev/null
+++ b/common/src/main/java/de/nicidienase/chaosflix/common/util/LiveEvent.kt
@@ -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)
\ No newline at end of file
diff --git a/common/src/main/java/de/nicidienase/chaosflix/common/util/SingleLiveEvent.kt b/common/src/main/java/de/nicidienase/chaosflix/common/util/SingleLiveEvent.kt
new file mode 100644
index 00000000..6a6fed8b
--- /dev/null
+++ b/common/src/main/java/de/nicidienase/chaosflix/common/util/SingleLiveEvent.kt
@@ -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"
+	}
+}
\ No newline at end of file
diff --git a/common/src/main/java/de/nicidienase/chaosflix/common/util/ThreadHandler.kt b/common/src/main/java/de/nicidienase/chaosflix/common/util/ThreadHandler.kt
new file mode 100644
index 00000000..c1354fd6
--- /dev/null
+++ b/common/src/main/java/de/nicidienase/chaosflix/common/util/ThreadHandler.kt
@@ -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
+	}
+}
\ No newline at end of file
diff --git a/common/src/main/res/values/urlstrings.xml b/common/src/main/res/values/urlstrings.xml
new file mode 100644
index 00000000..64664bae
--- /dev/null
+++ b/common/src/main/res/values/urlstrings.xml
@@ -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>
\ No newline at end of file