Bimba.git

commit d1852de8986654203668ccca53ca6599132133a8

Author: Adam Pioterek <adam.pioterek@protonmail.ch>

downloading timetable

%!v(PANIC=String method: strings: negative Repeat count)


diff --git a/app/build.gradle b/app/build.gradle
index 2e38408235a731b14219e2edd70096a7be39d188..2088d6ce90688cb51c30cd965bbd14dacfd8da5b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -36,7 +36,6 @@     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
     implementation 'com.github.arimorty:floatingsearchview:2.1.1'
     implementation 'com.google.code.gson:gson:2.8.1'
     implementation 'com.squareup.okhttp3:okhttp:3.8.1'
-    implementation 'com.github.ghost1372:Mzip-Android:0.4.0'
     implementation 'io.requery:sqlite-android:3.22.0'
     implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.5'
     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.5"




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
index 29d8b1cecf62d180a40890da4190c8f48404098f..683b8373f85d34425892e6e9e46d403f01826df5 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
@@ -23,6 +23,8 @@ import ml.adamsprogs.bimba.models.suggestions.*
 
 import com.arlib.floatingsearchview.FloatingSearchView
 import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
+import ml.adamsprogs.bimba.collections.FavouriteStorage
+import ml.adamsprogs.bimba.models.adapters.FavouritesAdapter
 
 //todo<p:1> searchView integration
 //todo something devours RAM
@@ -275,7 +277,6 @@     }
 
     override fun onTimetableDownload(result: String?) {
         val message: String = when (result) {
-            TimetableDownloader.RESULT_DOWNLOADED -> getString(R.string.refreshing_cache)
             TimetableDownloader.RESULT_NO_CONNECTIVITY -> getString(R.string.no_connectivity)
             TimetableDownloader.RESULT_UP_TO_DATE -> getString(R.string.timetable_up_to_date)
             TimetableDownloader.RESULT_FINISHED -> getString(R.string.timetable_downloaded)




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt
index 06d645b1b8e90386e934e7ed8b43289a5553cdf3..372f6448099ab199590f3e2e2241039aff3c3af1 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt
@@ -7,8 +7,8 @@ import android.support.v7.widget.LinearLayoutManager
 import android.widget.EditText
 import ml.adamsprogs.bimba.R
 import ml.adamsprogs.bimba.models.Favourite
-import ml.adamsprogs.bimba.models.FavouriteEditRowAdapter
-import ml.adamsprogs.bimba.models.FavouriteStorage
+import ml.adamsprogs.bimba.models.adapters.FavouriteEditRowAdapter
+import ml.adamsprogs.bimba.collections.FavouriteStorage
 import kotlinx.android.synthetic.main.activity_edit_favourite.*
 import android.app.Activity
 import android.content.Intent




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
index 5bd0095df723e6c3629556cd2b2a0f41debc5caa..91c215fc7f8a067541bcc9c03258c8f4ecd07ba7 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
@@ -77,8 +77,6 @@                 networkStateReceiver.removeOnConnectivityChangeListener(this)
                 startActivity(Intent(this, DashActivity::class.java))
                 finish()
             }
-            TimetableDownloader.RESULT_DOWNLOADED ->
-                no_db_caption.text = getString(R.string.timetable_converting)
             else -> no_db_caption.text = getString(R.string.error_try_later)
         }
     }




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
index d5c41453695eee6011f0541d8028e1a3a4afda8a..de7311f4afc699144bab997b1efed17f81366798 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
@@ -13,9 +13,11 @@
 import java.util.Calendar
 import kotlinx.android.synthetic.main.activity_stop.*
 import ml.adamsprogs.bimba.*
+import ml.adamsprogs.bimba.collections.FavouriteStorage
 import ml.adamsprogs.bimba.datasources.*
 import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
 import ml.adamsprogs.bimba.models.*
+import ml.adamsprogs.bimba.models.adapters.DeparturesAdapter
 
 class StopActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, MessageReceiver.OnVmListener, Favourite.OnVmPreparedListener {
 
@@ -170,7 +172,6 @@     private fun ticked() = Calendar.getInstance().timeInMillis - lastUpdated >= VmClient.TICK_6_ZINA_TIM_WITH_MARGIN
 
     override fun onTimetableDownload(result: String?) {
         val message: String = when (result) {
-            TimetableDownloader.RESULT_DOWNLOADED -> getString(R.string.refreshing_cache)
             TimetableDownloader.RESULT_NO_CONNECTIVITY -> getString(R.string.no_connectivity)
             TimetableDownloader.RESULT_UP_TO_DATE -> getString(R.string.timetable_up_to_date)
             TimetableDownloader.RESULT_FINISHED -> getString(R.string.timetable_downloaded)




diff --git a/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt b/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt
new file mode 100644
index 0000000000000000000000000000000000000000..83d651369fecf9bf9e7798cc12a262885ea6e2f1
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt
@@ -0,0 +1,180 @@
+package ml.adamsprogs.bimba.collections
+
+import android.content.*
+import com.google.gson.*
+import ml.adamsprogs.bimba.*
+import ml.adamsprogs.bimba.models.*
+import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
+import java.util.Calendar
+
+
+class FavouriteStorage private constructor(context: Context) : Iterable<Favourite> {
+    companion object {
+        private var favouriteStorage: FavouriteStorage? = null
+        fun getFavouriteStorage(context: Context? = null): FavouriteStorage {
+            return if (favouriteStorage == null) {
+                if (context == null)
+                    throw IllegalArgumentException("requested new storage appContext not given")
+                else {
+                    favouriteStorage = FavouriteStorage(context)
+                    favouriteStorage as FavouriteStorage
+                }
+            } else
+                favouriteStorage as FavouriteStorage
+        }
+    }
+
+    val favourites = HashMap<String, Favourite>()
+    private val positionIndex = IndexableTreeSet<String>()
+    private val preferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE)
+
+    init {
+        val favouritesString = preferences.getString("favourites", "{}")
+        val favouritesMap = Gson().fromJson(favouritesString, JsonObject::class.java)
+        for ((name, jsonTimetables) in favouritesMap.entrySet()) {
+            val timetables = HashSet<StopSegment>()
+            jsonTimetables.asJsonArray.mapTo(timetables) {
+                val stopSegment = StopSegment(AgencyAndId(it.asJsonObject["stop"].asString), null)
+                val plates = HashSet<Plate.ID>()
+                it.asJsonObject["plates"].asJsonArray.mapTo(plates) {
+                    Plate.ID(AgencyAndId(it.asJsonObject["line"].asString),
+                            AgencyAndId(it.asJsonObject["stop"].asString),
+                            it.asJsonObject["headsign"].asString)
+                }
+                stopSegment.plates = plates
+                stopSegment
+            }
+            favourites[name] = Favourite(name, timetables)
+            positionIndex.add(name)
+        }
+    }
+
+    override fun iterator(): Iterator<Favourite> = favourites.values.iterator()
+
+    fun has(name: String): Boolean = favourites.contains(name)
+
+    fun add(name: String, timetables: HashSet<StopSegment>) {
+        if (favourites[name] == null) {
+            favourites[name] = Favourite(name, timetables)
+            addIndex(name)
+            serialize()
+        }
+    }
+
+    fun add(name: String, favourite: Favourite) {
+        if (favourites[name] == null) {
+            favourites[name] = favourite
+            addIndex(name)
+            serialize()
+        }
+    }
+
+    private fun addIndex(name:String) {
+        positionIndex.apply {
+            this.add(name)
+        }
+    }
+
+    fun delete(name: String) {
+        favourites.remove(name)
+        positionIndex.remove(name)
+        serialize()
+    }
+
+    fun delete(name: String, plate: Plate.ID) {
+        favourites[name]?.delete(plate)
+        serialize()
+    }
+
+    private fun serialize() {
+        val rootObject = JsonObject()
+        for ((name, favourite) in favourites) {
+            val timetables = JsonArray()
+            for (timetable in favourite.segments) {
+                val segment = JsonObject()
+                segment.addProperty("stop", timetable.stop.id)
+                val plates = JsonArray()
+                for (plate in timetable.plates ?: HashSet()) {
+                    val element = JsonObject()
+                    element.addProperty("stop", plate.stop.id)
+                    element.addProperty("line", plate.line.id)
+                    element.addProperty("headsign", plate.headsign)
+                    plates.add(element)
+                }
+                segment.add("plates", plates)
+                timetables.add(segment)
+            }
+            rootObject.add(name, timetables)
+        }
+        val favouritesString = Gson().toJson(rootObject)
+        val editor = preferences.edit()
+        editor.putString("favourites", favouritesString)
+        editor.apply()
+
+    }
+
+    fun merge(names: List<String>) {
+        if (names.size < 2)
+            return
+
+        val newCache = HashMap<AgencyAndId, ArrayList<Departure>>()
+        names.forEach {
+            favourites[it]!!.fullDepartures.forEach {
+                if (newCache[it.key] == null)
+                    newCache[it.key] = ArrayList()
+                newCache[it.key]!!.addAll(it.value)
+            }
+        }
+        val now = Calendar.getInstance().secondsAfterMidnight()
+        newCache.forEach {
+            it.value.sortBy { it.timeTill(now) }
+        }
+        val newFavourite = Favourite(names[0], HashSet(), newCache)
+        for (name in names) {
+            newFavourite.segments.addAll(favourites[name]!!.segments)
+            favourites.remove(name)
+            positionIndex.remove(name)
+        }
+        favourites[names[0]] = newFavourite
+        addIndex(names[0])
+
+        serialize()
+    }
+
+    fun rename(oldName: String, newName: String) {
+        val favourite = favourites[oldName] ?: return
+        favourite.rename(newName)
+        favourites.remove(oldName)
+        positionIndex.remove(oldName)
+        favourites[newName] = favourite
+        addIndex(newName)
+        serialize()
+    }
+
+    fun registerOnVm(receiver: MessageReceiver, context: Context) {
+        favourites.values.forEach {
+            it.registerOnVm(receiver, context)
+        }
+    }
+
+    fun deregisterOnVm(receiver: MessageReceiver, context: Context) {
+        favourites.values.forEach {
+            it.deregisterOnVm(receiver, context)
+        }
+    }
+
+    operator fun get(name: String): Favourite? {
+        return favourites[name]
+    }
+
+    operator fun get(position: Int): Favourite? {
+        return favourites[positionIndex[position]]
+    }
+
+    fun indexOf(name: String): Int {
+        return positionIndex.indexOf(name)
+    }
+
+    val size
+        get() = favourites.size
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/collections/IndexableTreeSet.kt b/app/src/main/java/ml/adamsprogs/bimba/collections/IndexableTreeSet.kt
new file mode 100644
index 0000000000000000000000000000000000000000..19719f1a61a394bd4cf61dcd35e09ac21a0dc08a
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/collections/IndexableTreeSet.kt
@@ -0,0 +1,11 @@
+package ml.adamsprogs.bimba.collections
+
+import java.util.TreeSet
+
+class IndexableTreeSet<T>: TreeSet<T>() {
+    operator fun get(position: Int): T {
+        @Suppress("UNCHECKED_CAST")
+        return this.toArray()[position] as T
+    }
+
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt
index a81b3802419bfeb5d983673227868ee67099a198..91c9cb88d3acc22103c40c3c18880e557db2b309 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt
@@ -1,28 +1,15 @@
 package ml.adamsprogs.bimba.datasources
 
 import android.annotation.TargetApi
-import android.app.IntentService
-import android.app.Notification
-import android.content.Context
-import android.content.Intent
+import android.app.*
+import android.content.*
 import android.support.v4.app.NotificationCompat
 import java.io.*
-import android.app.NotificationManager
 import android.os.Build
-import com.google.gson.Gson
-import com.google.gson.JsonArray
-import com.google.gson.JsonObject
-import ir.mahdi.mzip.zip.ZipArchive
-import ml.adamsprogs.bimba.NetworkStateReceiver
-import ml.adamsprogs.bimba.NotificationChannels
-import ml.adamsprogs.bimba.R
-import ml.adamsprogs.bimba.getSecondaryExternalFilesDir
-import ml.adamsprogs.bimba.models.Timetable
-import java.net.ConnectException
-import java.net.URL
-import java.util.Calendar
+import ml.adamsprogs.bimba.*
+import java.net.*
+import java.util.zip.GZIPInputStream
 import javax.net.ssl.HttpsURLConnection
-import kotlin.collections.*
 
 class TimetableDownloader : IntentService("TimetableDownloader") {
     companion object {
@@ -31,12 +18,12 @@         const val EXTRA_FORCE = "force"
         const val EXTRA_RESULT = "result"
         const val RESULT_NO_CONNECTIVITY = "no connectivity"
         const val RESULT_UP_TO_DATE = "up-to-date"
-        const val RESULT_DOWNLOADED = "downloaded"
         const val RESULT_FINISHED = "finished"
     }
 
     private lateinit var notificationManager: NotificationManager
-    private var size: Int = 0
+    private var sizeCompressed: Int = 0
+    private var sizeUncompressed: Int = 0
 
     override fun onHandleIntent(intent: Intent?) {
 
@@ -48,14 +35,14 @@                 sendResult(RESULT_NO_CONNECTIVITY)
                 return
             }
 
-            //todo download timetable
-            sendResult(RESULT_UP_TO_DATE)
-            return
+            val localETag = prefs.getString("etag", "")
 
             val httpCon: HttpsURLConnection
             try {
-                val url = URL("https://adamsprogs.ml/gtfs")
+                val url = URL("https://adamsprogs.ml/gtfs") //todo if https fails -> http
                 httpCon = url.openConnection() as HttpsURLConnection
+                httpCon.addRequestProperty("ETag", localETag)
+                httpCon.connect()
                 if (httpCon.responseCode == HttpsURLConnection.HTTP_NOT_MODIFIED) {
                     sendResult(RESULT_UP_TO_DATE)
                     return
@@ -75,44 +62,26 @@                 sendResult(RESULT_NO_CONNECTIVITY)
                 return
             }
 
-            val lastModified = httpCon.getHeaderField("Content-Disposition").split("=")[1].trim('\"').split("_")[0]
-            size = httpCon.getHeaderField("Content-Length").toInt() / 1024
+            val newETag = httpCon.getHeaderField("ETag")
+            sizeCompressed = httpCon.getHeaderField("Content-Length").toInt() / 1024
+            sizeUncompressed = httpCon.getHeaderField("X-Uncompressed-Content-Length").toInt() / 1024
 
-            val force = intent.getBooleanExtra(EXTRA_FORCE, false)
-            val currentLastModified = prefs.getString("timetableLastModified", "19791012")
-            if (lastModified <= currentLastModified && !force) {
-                sendResult(RESULT_UP_TO_DATE)
-                return
-            }
+            notify(0, R.string.timetable_downloading, R.string.timetable_uncompressing, sizeCompressed, sizeUncompressed)
+
 
-            notify(0, getString(R.string.timetable_downloading), size)
+            val gtfsDb = File(getSecondaryExternalFilesDir(), "timetable.db")
 
+            val inputStream = httpCon.inputStream
+            val gzipInputStream = GZIPInputStream(inputStream)
+            val outputStream = FileOutputStream(gtfsDb)
+
+            gzipInputStream.listenableCopyTo(outputStream) {
+                notify((it / 1024).toInt(), R.string.timetable_downloading, R.string.timetable_uncompressing, sizeCompressed, sizeUncompressed)
+            }
 
-            val gtfs = File(getSecondaryExternalFilesDir(), "timetable.zip")
-            copyInputStreamToFile(httpCon.inputStream, gtfs)
             val prefsEditor = prefs.edit()
-            prefsEditor.putString("timetableLastModified", lastModified)
+            prefsEditor.putString("etag", newETag)
             prefsEditor.apply()
-            sendResult(RESULT_DOWNLOADED)
-
-            notify(getString(R.string.timetable_converting))
-
-            val target = File(getSecondaryExternalFilesDir(), "gtfs_files")
-            target.deleteRecursively()
-            target.mkdir()
-            ZipArchive.unzip(gtfs.path, target.path, "")
-
-            val string = getString(R.string.timetable_converting)
-            notify(0, string, 1_030_000)
-
-            println(Calendar.getInstance().timeInMillis)
-
-            gtfs.delete()
-
-            createIndices()
-            Timetable.getTimetable(this).refresh()
-
-            println(Calendar.getInstance().timeInMillis)
 
             cancelNotification()
 
@@ -120,73 +89,6 @@             sendResult(RESULT_FINISHED)
         }
     }
 
-    private fun createIndices() {
-        /*val settings = CsvParserSettings()
-        settings.format.setLineSeparator("\r\n")
-        settings.format.quote = '"'
-        settings.isHeaderExtractionEnabled = true
-
-        val parser = CsvParser(settings)
-
-        val stopIndexFile = File(getSecondaryExternalFilesDir(), "gtfs_files/stop_index.yml")
-        val tripIndexFile = File(getSecondaryExternalFilesDir(), "gtfs_files/trip_index.yml")
-
-        val stopsIndex = HashMap<String, List<Long>>()
-        val tripsIndex = HashMap<String, List<Long>>()
-
-        parser.parseAll(File(getSecondaryExternalFilesDir(), "gtfs_files/trips.txt")).forEach {
-            tripsIndex[it[2]] = ArrayList()
-        }
-
-        parser.parseAll(File(getSecondaryExternalFilesDir(), "gtfs_files/stops.txt")).forEach {
-            stopsIndex[it[0]] = ArrayList()
-        }
-
-        val string = getString(R.string.timetable_converting)
-
-        parser.beginParsing(File(getSecondaryExternalFilesDir(), "gtfs_files/stop_times.txt"))
-        var line: Array<String>? = null
-        while ({ line = parser.parseNext(); line }() != null) {
-            val lineNumber = parser.appContext.currentLine()
-            (tripsIndex[line!![0]] as ArrayList).add(lineNumber)
-            (stopsIndex[line!![3]] as ArrayList).add(lineNumber)
-            if (lineNumber % 10_300 == 0L)
-                notify(lineNumber.toInt(), string, 1_030_000)
-        }
-
-        println(Calendar.getInstance().timeInMillis)
-        stopsIndex.filter { it.value.contains(0) }.forEach { println("${it.key}: ${it.value.joinToString()}") }
-        println(Calendar.getInstance().timeInMillis)
-
-        serialiseIndex(stopsIndex, stopIndexFile)
-        serialiseIndex(tripsIndex, tripIndexFile)
-        */
-    }
-
-    private fun serialiseIndex(index: HashMap<String, List<Long>>, file: File) {
-        val stopsRootObject = JsonObject()
-        index.forEach {
-            val stop = JsonArray()
-            it.value.forEach {
-                stop.add(it)
-            }
-            stopsRootObject.add(it.key, stop)
-        }
-
-        val writer = BufferedWriter(file.writer())
-        writer.write(Gson().toJson(stopsRootObject))
-        writer.close()
-    }
-
-    private fun today(): String {
-        val cal = Calendar.getInstance()
-        val d = cal[Calendar.DAY_OF_MONTH]
-        val m = cal[Calendar.MONTH] + 1
-        val y = cal[Calendar.YEAR]
-
-        return "%d%02d%02d".format(y, m, d)
-    }
-
     private fun sendResult(result: String) {
         val broadcastIntent = Intent()
         broadcastIntent.action = ACTION_DOWNLOADED
@@ -195,19 +97,23 @@         broadcastIntent.putExtra(EXTRA_RESULT, result)
         sendBroadcast(broadcastIntent)
     }
 
-    private fun notify(progress: Int, message: String, max: Int) {
+    private fun notify(progress: Int, titleId: Int, messageId: Int, sizeCompressed: Int, sizeUncompressed: Int) {
+        val quotient = sizeCompressed.toFloat() / sizeUncompressed.toFloat()
+        val message = getString(messageId, Math.max(progress * quotient, sizeCompressed.toFloat()),
+                sizeCompressed, (progress.toFloat() / sizeUncompressed.toFloat()) * 100)
+        val title = getString(titleId)
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
-            notifyCompat(progress, message, max)
+            notifyCompat(progress, title, message, sizeUncompressed)
         else
-            notifyStandard(progress, message, max)
+            notifyStandard(progress, title, message, sizeUncompressed)
     }
 
     @Suppress("DEPRECATION")
-    private fun notifyCompat(progress: Int, message: String, max: Int) {
+    private fun notifyCompat(progress: Int, title: String, message: String, max: Int) {
         val builder = NotificationCompat.Builder(this)
                 .setSmallIcon(R.drawable.ic_download)
-                .setContentTitle(message)
-                .setContentText("${(progress.toDouble() / max.toDouble() * 100).toInt()} %")
+                .setContentTitle(title)
+                .setContentText(message)
                 .setCategory(NotificationCompat.CATEGORY_PROGRESS)
                 .setOngoing(true)
                 .setProgress(max, progress, false)
@@ -215,74 +121,19 @@         notificationManager.notify(42, builder.build())
     }
 
     @TargetApi(Build.VERSION_CODES.O)
-    private fun notifyStandard(progress: Int, message: String, max: Int) {
+    private fun notifyStandard(progress: Int, title: String, message: String, max: Int) {
         NotificationChannels.makeChannel(NotificationChannels.CHANNEL_UPDATES, "Updates", notificationManager)
         val builder = Notification.Builder(this, NotificationChannels.CHANNEL_UPDATES)
                 .setSmallIcon(R.drawable.ic_download)
-                .setContentTitle(message)
-                .setContentText("${(progress.toDouble() / max.toDouble() * 100).toInt()} %")
+                .setContentTitle(title)
+                .setContentText(message)
                 .setCategory(Notification.CATEGORY_PROGRESS)
                 .setOngoing(true)
                 .setProgress(max, progress, false)
         notificationManager.notify(42, builder.build())
     }
 
-    private fun notify(message: String) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
-            notifyCompat(message)
-        else
-            notifyStandard(message)
-
-    }
-
-    @Suppress("DEPRECATION")
-    private fun notifyCompat(message: String) {
-        val builder = NotificationCompat.Builder(this)
-                .setSmallIcon(R.drawable.ic_download)
-                .setContentTitle(message)
-                .setContentText("")
-                .setCategory(NotificationCompat.CATEGORY_PROGRESS)
-                .setOngoing(true)
-                .setProgress(0, 0, true)
-        notificationManager.notify(42, builder.build())
-    }
-
-    @TargetApi(Build.VERSION_CODES.O)
-    private fun notifyStandard(message: String) {
-        NotificationChannels.makeChannel(NotificationChannels.CHANNEL_UPDATES, "Updates", notificationManager)
-        val builder = Notification.Builder(this, NotificationChannels.CHANNEL_UPDATES)
-                .setSmallIcon(R.drawable.ic_download)
-                .setContentTitle(message)
-                .setContentText("")
-                .setCategory(Notification.CATEGORY_PROGRESS)
-                .setOngoing(true)
-                .setProgress(0, 0, true)
-        notificationManager.notify(42, builder.build())
-    }
-
     private fun cancelNotification() {
         notificationManager.cancel(42)
-    }
-
-    private fun copyInputStreamToFile(ins: InputStream, file: File) {
-        try {
-            val out = FileOutputStream(file)
-            val buf = ByteArray(5 * 1024)
-            var lenSum = 0.0f
-            var len = 42
-            while (len > 0) {
-                len = ins.read(buf)
-                if (len <= 0)
-                    break
-                out.write(buf, 0, len)
-                lenSum += len.toFloat() / 1024.0f
-                notify(lenSum.toInt(), getString(R.string.timetable_downloading), size)
-            }
-            out.close()
-        } catch (e: Exception) {
-            e.printStackTrace()
-        } finally {
-            ins.close()
-        }
     }
 }




diff --git a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
index 936c1475dcc3dff606bfe123ed1fae3ad14e7b1f..869d2e6b5201d15a2da4f46ef93f9fce2111227c 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
@@ -5,7 +5,7 @@ import android.content.Context
 import android.graphics.drawable.Drawable
 import android.os.Build
 import ml.adamsprogs.bimba.activities.StopActivity
-import java.io.File
+import java.io.*
 import java.text.SimpleDateFormat
 import java.util.*
 import kotlin.collections.ArrayList
@@ -81,3 +81,16 @@     val dirs = this.getExternalFilesDirs(null)
     return dirs[0]
 //    return dirs[dirs.size - 1]
 }
+
+internal fun InputStream.listenableCopyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE, listener: (Long) -> Unit): Long {
+    var bytesCopied: Long = 0
+    val buffer = ByteArray(bufferSize)
+    var bytes = read(buffer)
+    while (bytes >= 0) {
+        out.write(buffer, 0, bytes)
+        bytesCopied += bytes
+        listener(bytesCopied)
+        bytes = read(buffer)
+    }
+    return bytesCopied
+}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt
deleted file mode 100644
index 457e6b09f404b66d2aefad7d99d3950fa14bf685..0000000000000000000000000000000000000000
--- a/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-package ml.adamsprogs.bimba.models
-
-import android.app.AlertDialog
-import android.content.Context
-import android.content.DialogInterface
-import android.support.v4.content.res.ResourcesCompat
-import android.support.v7.widget.RecyclerView
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.TextView
-import ml.adamsprogs.bimba.R
-import android.view.LayoutInflater
-import ml.adamsprogs.bimba.Declinator
-import ml.adamsprogs.bimba.rollTime
-import java.util.*
-
-//todo<p:1> on click show time (HH:MM)
-class DeparturesAdapter(val context: Context, private val departures: List<Departure>?, private val relativeTime: Boolean) :
-        RecyclerView.Adapter<DeparturesAdapter.ViewHolder>() {
-
-    companion object {
-        const val VIEW_TYPE_LOADING: Int = 0
-        const val VIEW_TYPE_CONTENT: Int = 1
-        const val VIEW_TYPE_EMPTY: Int = 2
-    }
-
-//    init {
-//        departures?.forEach {
-//            println("${it.line} -> ${it.headsign} @${it.time} (${if (it.isModified) it.modification[0] else{} })")
-//        }
-//    }
-
-    override fun getItemCount(): Int {
-        if (departures == null || departures.isEmpty())
-            return 1
-        return departures.size
-    }
-
-    override fun getItemViewType(position: Int): Int {
-        return when {
-            departures == null -> VIEW_TYPE_EMPTY
-            departures.isEmpty() -> VIEW_TYPE_LOADING
-            else -> VIEW_TYPE_CONTENT
-        }
-    }
-
-    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
-        if (departures == null) {
-            return
-        }
-        val line = holder.lineTextView
-        val time = holder.timeTextView
-        val direction = holder.directionTextView
-        if (departures.isEmpty()) {
-            time.text = context.getString(R.string.no_departures)
-            return
-        }
-        val departure = departures[position]
-        //println("${departure.line} -> ${departure.headsign} @${departure.time} (${if (departure.isModified) departure.modification[0] else {}})")
-        val now = Calendar.getInstance()
-        val departureTime = Calendar.getInstance().rollTime(departure.time)
-        if (departure.tomorrow)
-            departureTime.add(Calendar.DAY_OF_MONTH, 1)
-
-        val departureIn = ((departureTime.timeInMillis - now.timeInMillis) / (1000 * 60)).toInt()
-        val timeString: String
-
-        timeString = if (departureIn > 60 || departureIn < 0 || !relativeTime)
-            context.getString(R.string.departure_at, "${String.format("%02d", departureTime.get(Calendar.HOUR_OF_DAY))}:${String.format("%02d", departureTime.get(Calendar.MINUTE))}")
-        else if (departureIn > 0 && !departure.onStop)
-            context.getString(Declinator.decline(departureIn), departureIn.toString())
-        else
-            context.getString(R.string.now)
-
-        line.text = departure.lineText
-        time.text = timeString
-        direction.text = context.getString(R.string.departure_to, departure.headsign)
-        val icon = holder.typeIcon
-        if (departure.vm)
-            icon.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_departure_vm, context.theme))
-        else
-            icon.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_departure_timetable, context.theme))
-
-        if (departure.lowFloor)
-            holder.floorIcon.visibility = View.VISIBLE
-        if (departure.isModified) {
-            holder.infoIcon.visibility = View.VISIBLE
-            holder.root.setOnClickListener {
-                AlertDialog.Builder(context)
-                        .setPositiveButton(context.getText(android.R.string.ok),
-                                { dialog: DialogInterface, _: Int -> dialog.cancel() })
-                        .setCancelable(true)
-                        .setMessage(departure.modification.joinToString("; "))
-                        .create().show()
-            }
-        }
-    }
-
-    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
-        val context = parent.context
-        val inflater = LayoutInflater.from(context)
-
-        val rowView = inflater.inflate(R.layout.row_departure, parent, false)
-        return ViewHolder(rowView)
-    }
-
-    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
-        val root = itemView.findViewById<View>(R.id.departureRow)!!
-        val lineTextView: TextView = itemView.findViewById(R.id.lineNumber)
-        val timeTextView: TextView = itemView.findViewById(R.id.departureTime)
-        val directionTextView: TextView = itemView.findViewById(R.id.departureDirection)
-        val typeIcon: ImageView = itemView.findViewById(R.id.departureTypeIcon)
-        val infoIcon: ImageView = itemView.findViewById(R.id.departureInfoIcon)
-        val floorIcon: ImageView = itemView.findViewById(R.id.departureFloorIcon)
-    }
-}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt
deleted file mode 100644
index 7e78913947deebee44524e88984d9e3d0b71f4bc..0000000000000000000000000000000000000000
--- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package ml.adamsprogs.bimba.models
-
-import android.support.v7.widget.RecyclerView
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.TextView
-import ml.adamsprogs.bimba.R
-
-class FavouriteEditRowAdapter(private var favourite: Favourite) :
-        RecyclerView.Adapter<FavouriteEditRowAdapter.ViewHolder>() {
-    override fun getItemCount(): Int {
-        return favourite.size
-    }
-
-    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
-        val timetable = Timetable.getTimetable()
-        val favourites = FavouriteStorage.getFavouriteStorage()
-        val id = favourite.segments.flatMap { it.plates!! }.sortedBy { "${it.line}${it.stop}"}[position]
-        val plate = Plate(id,null)
-        val favouriteElement = "${timetable.getStopName(plate.id.stop)} ( ${timetable.getStopCode(plate.id.stop)}):\n${plate.id.line} → ${plate.id.headsign}"
-        holder.rowTextView.text = favouriteElement
-//        holder?.splitButton?.setOnClickListener {
-//            favourites.detach(favourite.name, id, favouriteElement)
-//            favourite = favourites.favourites[favourite.name]!!
-//            notifyDataSetChanged()
-//        }
-        holder.deleteButton.setOnClickListener {
-            favourites.delete(favourite.name, id)
-            favourite = favourites.favourites[favourite.name]!!
-            notifyDataSetChanged()
-        }
-    }
-
-    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
-        val context = parent.context
-        val inflater = LayoutInflater.from(context)
-
-        val rowView = inflater.inflate(R.layout.row_favourite_edit, parent, false)
-        return ViewHolder(rowView)
-    }
-
-    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
-        val rowTextView:TextView = itemView.findViewById(R.id.favourite_edit_row)
-//        val splitButton:ImageView = itemView.findViewById(R.id.favourite_edit_split)
-        val deleteButton:ImageView = itemView.findViewById(R.id.favourite_edit_delete)
-    }
-}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt
deleted file mode 100644
index 65dee0ce3208e8e493b450ffc3d4fa8c1b80d51d..0000000000000000000000000000000000000000
--- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-package ml.adamsprogs.bimba.models
-
-import android.content.Context
-import android.content.SharedPreferences
-import com.google.gson.Gson
-import com.google.gson.JsonArray
-import com.google.gson.JsonObject
-import ml.adamsprogs.bimba.MessageReceiver
-import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
-import ml.adamsprogs.bimba.secondsAfterMidnight
-import java.util.Calendar
-
-
-class FavouriteStorage private constructor(context: Context) : Iterable<Favourite> {
-    companion object {
-        private var favouriteStorage: FavouriteStorage? = null
-        fun getFavouriteStorage(context: Context? = null): FavouriteStorage {
-            return if (favouriteStorage == null) {
-                if (context == null)
-                    throw IllegalArgumentException("requested new storage appContext not given")
-                else {
-                    favouriteStorage = FavouriteStorage(context)
-                    favouriteStorage as FavouriteStorage
-                }
-            } else
-                favouriteStorage as FavouriteStorage
-        }
-    }
-
-    val favourites = HashMap<String, Favourite>()
-    private val positionIndex = ArrayList<String>()
-    private val preferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE)
-
-    init {
-        val favouritesString = preferences.getString("favourites", "{}")
-        val favouritesMap = Gson().fromJson(favouritesString, JsonObject::class.java)
-        for ((name, jsonTimetables) in favouritesMap.entrySet()) {
-            val timetables = HashSet<StopSegment>()
-            jsonTimetables.asJsonArray.mapTo(timetables) {
-                val stopSegment = StopSegment(AgencyAndId(it.asJsonObject["stop"].asString), null)
-                val plates = HashSet<Plate.ID>()
-                it.asJsonObject["plates"].asJsonArray.mapTo(plates) {
-                    Plate.ID(AgencyAndId(it.asJsonObject["line"].asString),
-                            AgencyAndId(it.asJsonObject["stop"].asString),
-                            it.asJsonObject["headsign"].asString)
-                }
-                stopSegment.plates = plates
-                stopSegment
-            }
-            favourites[name] = Favourite(name, timetables)
-            positionIndex.add(name)
-        }
-        positionIndex.sort()
-    }
-
-    override fun iterator(): Iterator<Favourite> = favourites.values.iterator()
-
-    fun has(name: String): Boolean = favourites.contains(name)
-
-    fun add(name: String, timetables: HashSet<StopSegment>) {
-        if (favourites[name] == null) {
-            favourites[name] = Favourite(name, timetables)
-            addIndex(name)
-            serialize()
-        }
-    }
-
-    fun add(name: String, favourite: Favourite) {
-        if (favourites[name] == null) {
-            favourites[name] = favourite
-            addIndex(name)
-            serialize()
-        }
-    }
-
-    private fun addIndex(name:String) {
-        positionIndex.apply {
-            this.add(name)
-            this.sort()
-        }
-    }
-
-    fun delete(name: String) {
-        favourites.remove(name)
-        positionIndex.remove(name)
-        serialize()
-    }
-
-    fun delete(name: String, plate: Plate.ID) {
-        favourites[name]?.delete(plate)
-        serialize()
-    }
-
-    private fun serialize() {
-        val rootObject = JsonObject()
-        for ((name, favourite) in favourites) {
-            val timetables = JsonArray()
-            for (timetable in favourite.segments) {
-                val segment = JsonObject()
-                segment.addProperty("stop", timetable.stop.id)
-                val plates = JsonArray()
-                for (plate in timetable.plates ?: HashSet()) {
-                    val element = JsonObject()
-                    element.addProperty("stop", plate.stop.id)
-                    element.addProperty("line", plate.line.id)
-                    element.addProperty("headsign", plate.headsign)
-                    plates.add(element)
-                }
-                segment.add("plates", plates)
-                timetables.add(segment)
-            }
-            rootObject.add(name, timetables)
-        }
-        val favouritesString = Gson().toJson(rootObject)
-        val editor = preferences.edit()
-        editor.putString("favourites", favouritesString)
-        editor.apply()
-
-    }
-
-    fun merge(names: List<String>) {
-        if (names.size < 2)
-            return
-
-        val newCache = HashMap<AgencyAndId, ArrayList<Departure>>()
-        names.forEach {
-            favourites[it]!!.fullDepartures.forEach {
-                if (newCache[it.key] == null)
-                    newCache[it.key] = ArrayList()
-                newCache[it.key]!!.addAll(it.value)
-            }
-        }
-        val now = Calendar.getInstance().secondsAfterMidnight()
-        newCache.forEach {
-            it.value.sortBy { it.timeTill(now) }
-        }
-        val newFavourite = Favourite(names[0], HashSet(), newCache)
-        for (name in names) {
-            newFavourite.segments.addAll(favourites[name]!!.segments)
-            favourites.remove(name)
-            positionIndex.remove(name)
-        }
-        favourites[names[0]] = newFavourite
-        addIndex(names[0])
-
-        serialize()
-    }
-
-    fun rename(oldName: String, newName: String) {
-        val favourite = favourites[oldName] ?: return
-        favourite.rename(newName)
-        favourites.remove(oldName)
-        positionIndex.remove(oldName)
-        favourites[newName] = favourite
-        addIndex(newName)
-        serialize()
-    }
-
-    fun registerOnVm(receiver: MessageReceiver, context: Context) {
-        favourites.values.forEach {
-            it.registerOnVm(receiver, context)
-        }
-    }
-
-    fun deregisterOnVm(receiver: MessageReceiver, context: Context) {
-        favourites.values.forEach {
-            it.deregisterOnVm(receiver, context)
-        }
-    }
-
-    operator fun get(name: String): Favourite? {
-        return favourites[name]
-    }
-
-    operator fun get(position: Int): Favourite? {
-        return favourites[positionIndex[position]]
-    }
-
-    fun indexOf(name: String): Int {
-        return positionIndex.indexOf(name)
-    }
-
-    val size
-        get() = favourites.size
-}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt
deleted file mode 100644
index 2643b27522224a6eab1474d0edbb427af0c8a72d..0000000000000000000000000000000000000000
--- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt
+++ /dev/null
@@ -1,144 +0,0 @@
-package ml.adamsprogs.bimba.models
-
-import android.content.Context
-import android.support.v4.content.res.ResourcesCompat
-import android.support.v7.widget.*
-import android.support.v7.widget.PopupMenu
-import android.util.SparseBooleanArray
-import android.view.*
-import android.widget.*
-import ml.adamsprogs.bimba.R
-import android.view.LayoutInflater
-import kotlinx.coroutines.experimental.CommonPool
-import kotlinx.coroutines.experimental.android.UI
-import kotlinx.coroutines.experimental.async
-import kotlinx.coroutines.experimental.launch
-import java.util.*
-import ml.adamsprogs.bimba.Declinator
-import ml.adamsprogs.bimba.secondsAfterMidnight
-
-
-class FavouritesAdapter(val appContext: Context, var favourites: FavouriteStorage,
-                        private val onMenuItemClickListener: OnMenuItemClickListener,
-                        private val onClickListener: ViewHolder.OnClickListener) :
-        RecyclerView.Adapter<FavouritesAdapter.ViewHolder>() {
-
-    private val selectedItems = SparseBooleanArray()
-
-    private fun isSelected(position: Int) = getSelectedItems().contains(position)
-
-    fun toggleSelection(position: Int) {
-        if (selectedItems.get(position, false)) {
-            selectedItems.delete(position)
-        } else {
-            selectedItems.put(position, true)
-        }
-        notifyItemChanged(position)
-    }
-
-    fun clearSelection() {
-        val selection = getSelectedItems()
-        selectedItems.clear()
-        for (i in selection) {
-            notifyItemChanged(i)
-        }
-    }
-
-    fun getSelectedItemCount() = selectedItems.size()
-
-    fun getSelectedItems(): List<Int> {
-        val items = ArrayList<Int>(selectedItems.size())
-        (0 until selectedItems.size()).mapTo(items) { selectedItems.keyAt(it) }
-        return items
-    }
-
-    override fun getItemCount() = favourites.size
-
-    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
-        launch(UI) {
-            val favourite = favourites[position]!!
-            holder.nameTextView.text = favourite.name
-
-            holder.selectedOverlay.visibility = if (isSelected(position)) View.VISIBLE else View.INVISIBLE
-            holder.moreButton.setOnClickListener {
-                val popup = PopupMenu(appContext, it)
-                val inflater = popup.menuInflater
-                popup.setOnMenuItemClickListener {
-                    when (it.itemId) {
-                        R.id.favourite_edit -> onMenuItemClickListener.edit(favourite.name)
-                        R.id.favourite_delete -> onMenuItemClickListener.delete(favourite.name)
-                        else -> false
-                    }
-                }
-                inflater.inflate(R.menu.favourite_actions, popup.menu)
-                popup.show()
-            }
-
-            val nextDeparture = async(CommonPool) {
-                favourite.nextDeparture()
-            }.await()
-
-            val nextDepartureText: String
-            val nextDepartureLineText: String
-            if (nextDeparture != null) {
-                val interval = nextDeparture.timeTill(Calendar.getInstance().secondsAfterMidnight())
-                nextDepartureLineText = appContext.getString(R.string.departure_to_line, nextDeparture.line, nextDeparture.headsign)
-                nextDepartureText = if (interval < 0)
-                    appContext.getString(R.string.just_departed)
-                else
-                    appContext.getString(Declinator.decline(interval), interval.toString())
-            } else {
-                nextDepartureText = appContext.getString(R.string.no_next_departure)
-                nextDepartureLineText = ""
-            }
-            holder.timeTextView.text = nextDepartureText
-            holder.lineTextView.text = nextDepartureLineText
-            if (nextDeparture != null) {
-                if (nextDeparture.vm)
-                    holder.typeIcon.setImageDrawable(ResourcesCompat.getDrawable(appContext.resources, R.drawable.ic_departure_vm, appContext.theme))
-                else
-                    holder.typeIcon.setImageDrawable(ResourcesCompat.getDrawable(appContext.resources, R.drawable.ic_departure_timetable, appContext.theme))
-            }
-        }
-    }
-
-    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
-        val context = parent.context
-        val inflater = LayoutInflater.from(context)
-
-        val rowView = inflater.inflate(R.layout.row_favourite, parent, false)
-        return ViewHolder(rowView, onClickListener)
-    }
-
-    class ViewHolder(itemView: View, private val listener: OnClickListener) : RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener {
-        override fun onLongClick(v: View?): Boolean {
-            return listener.onItemLongClicked(adapterPosition)
-        }
-
-        override fun onClick(v: View?) {
-            listener.onItemClicked(adapterPosition)
-        }
-
-        val selectedOverlay: View = itemView.findViewById(R.id.selected_overlay)
-        val nameTextView: TextView = itemView.findViewById(R.id.favourite_name)
-        val timeTextView: TextView = itemView.findViewById(R.id.favourite_time)
-        val lineTextView: TextView = itemView.findViewById(R.id.favourite_line)
-        val moreButton: ImageView = itemView.findViewById(R.id.favourite_more_button)
-        val typeIcon: ImageView = itemView.findViewById(R.id.departureTypeIcon)
-
-        init {
-            itemView.setOnClickListener(this)
-            itemView.setOnLongClickListener(this)
-        }
-
-        interface OnClickListener {
-            fun onItemClicked(position: Int)
-            fun onItemLongClicked(position: Int): Boolean
-        }
-    }
-
-    interface OnMenuItemClickListener {
-        fun edit(name: String): Boolean
-        fun delete(name: String): Boolean
-    }
-}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fce1683f0fddeeefd30ec9ce69e2bc938bddc237
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt
@@ -0,0 +1,118 @@
+package ml.adamsprogs.bimba.models.adapters
+
+import android.app.AlertDialog
+import android.content.Context
+import android.content.DialogInterface
+import android.support.v4.content.res.ResourcesCompat
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import ml.adamsprogs.bimba.R
+import android.view.LayoutInflater
+import ml.adamsprogs.bimba.Declinator
+import ml.adamsprogs.bimba.models.Departure
+import ml.adamsprogs.bimba.rollTime
+import java.util.*
+
+//todo<p:1> on click show time (HH:MM)
+class DeparturesAdapter(val context: Context, private val departures: List<Departure>?, private val relativeTime: Boolean) :
+        RecyclerView.Adapter<DeparturesAdapter.ViewHolder>() {
+
+    companion object {
+        const val VIEW_TYPE_LOADING: Int = 0
+        const val VIEW_TYPE_CONTENT: Int = 1
+        const val VIEW_TYPE_EMPTY: Int = 2
+    }
+
+//    init {
+//        departures?.forEach {
+//            println("${it.line} -> ${it.headsign} @${it.time} (${if (it.isModified) it.modification[0] else{} })")
+//        }
+//    }
+
+    override fun getItemCount(): Int {
+        if (departures == null || departures.isEmpty())
+            return 1
+        return departures.size
+    }
+
+    override fun getItemViewType(position: Int): Int {
+        return when {
+            departures == null -> VIEW_TYPE_EMPTY
+            departures.isEmpty() -> VIEW_TYPE_LOADING
+            else -> VIEW_TYPE_CONTENT
+        }
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        if (departures == null) {
+            return
+        }
+        val line = holder.lineTextView
+        val time = holder.timeTextView
+        val direction = holder.directionTextView
+        if (departures.isEmpty()) {
+            time.text = context.getString(R.string.no_departures)
+            return
+        }
+        val departure = departures[position]
+        //println("${departure.line} -> ${departure.headsign} @${departure.time} (${if (departure.isModified) departure.modification[0] else {}})")
+        val now = Calendar.getInstance()
+        val departureTime = Calendar.getInstance().rollTime(departure.time)
+        if (departure.tomorrow)
+            departureTime.add(Calendar.DAY_OF_MONTH, 1)
+
+        val departureIn = ((departureTime.timeInMillis - now.timeInMillis) / (1000 * 60)).toInt()
+        val timeString: String
+
+        timeString = if (departureIn > 60 || departureIn < 0 || !relativeTime)
+            context.getString(R.string.departure_at, "${String.format("%02d", departureTime.get(Calendar.HOUR_OF_DAY))}:${String.format("%02d", departureTime.get(Calendar.MINUTE))}")
+        else if (departureIn > 0 && !departure.onStop)
+            context.getString(Declinator.decline(departureIn), departureIn.toString())
+        else
+            context.getString(R.string.now)
+
+        line.text = departure.lineText
+        time.text = timeString
+        direction.text = context.getString(R.string.departure_to, departure.headsign)
+        val icon = holder.typeIcon
+        if (departure.vm)
+            icon.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_departure_vm, context.theme))
+        else
+            icon.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_departure_timetable, context.theme))
+
+        if (departure.lowFloor)
+            holder.floorIcon.visibility = View.VISIBLE
+        if (departure.isModified) {
+            holder.infoIcon.visibility = View.VISIBLE
+            holder.root.setOnClickListener {
+                AlertDialog.Builder(context)
+                        .setPositiveButton(context.getText(android.R.string.ok),
+                                { dialog: DialogInterface, _: Int -> dialog.cancel() })
+                        .setCancelable(true)
+                        .setMessage(departure.modification.joinToString("; "))
+                        .create().show()
+            }
+        }
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        val context = parent.context
+        val inflater = LayoutInflater.from(context)
+
+        val rowView = inflater.inflate(R.layout.row_departure, parent, false)
+        return ViewHolder(rowView)
+    }
+
+    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+        val root = itemView.findViewById<View>(R.id.departureRow)!!
+        val lineTextView: TextView = itemView.findViewById(R.id.lineNumber)
+        val timeTextView: TextView = itemView.findViewById(R.id.departureTime)
+        val directionTextView: TextView = itemView.findViewById(R.id.departureDirection)
+        val typeIcon: ImageView = itemView.findViewById(R.id.departureTypeIcon)
+        val infoIcon: ImageView = itemView.findViewById(R.id.departureInfoIcon)
+        val floorIcon: ImageView = itemView.findViewById(R.id.departureFloorIcon)
+    }
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..29ce0c2a7c8880ee71be0409fa4034ef5b16a6ad
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt
@@ -0,0 +1,53 @@
+package ml.adamsprogs.bimba.models.adapters
+
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import ml.adamsprogs.bimba.R
+import ml.adamsprogs.bimba.collections.FavouriteStorage
+import ml.adamsprogs.bimba.models.Favourite
+import ml.adamsprogs.bimba.models.Plate
+import ml.adamsprogs.bimba.models.Timetable
+
+class FavouriteEditRowAdapter(private var favourite: Favourite) :
+        RecyclerView.Adapter<FavouriteEditRowAdapter.ViewHolder>() {
+    override fun getItemCount(): Int {
+        return favourite.size
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        val timetable = Timetable.getTimetable()
+        val favourites = FavouriteStorage.getFavouriteStorage()
+        val id = favourite.segments.flatMap { it.plates!! }.sortedBy { "${it.line}${it.stop}"}[position]
+        val plate = Plate(id, null)
+        val favouriteElement = "${timetable.getStopName(plate.id.stop)} ( ${timetable.getStopCode(plate.id.stop)}):\n${plate.id.line} → ${plate.id.headsign}"
+        holder.rowTextView.text = favouriteElement
+//        holder?.splitButton?.setOnClickListener {
+//            favourites.detach(favourite.name, id, favouriteElement)
+//            favourite = favourites.favourites[favourite.name]!!
+//            notifyDataSetChanged()
+//        }
+        holder.deleteButton.setOnClickListener {
+            favourites.delete(favourite.name, id)
+            favourite = favourites.favourites[favourite.name]!!
+            notifyDataSetChanged()
+        }
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        val context = parent.context
+        val inflater = LayoutInflater.from(context)
+
+        val rowView = inflater.inflate(R.layout.row_favourite_edit, parent, false)
+        return ViewHolder(rowView)
+    }
+
+    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+        val rowTextView:TextView = itemView.findViewById(R.id.favourite_edit_row)
+//        val splitButton:ImageView = itemView.findViewById(R.id.favourite_edit_split)
+        val deleteButton:ImageView = itemView.findViewById(R.id.favourite_edit_delete)
+    }
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6cef0867376e4bcf1b8aecf4df91fc3b683ede7f
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt
@@ -0,0 +1,145 @@
+package ml.adamsprogs.bimba.models.adapters
+
+import android.content.Context
+import android.support.v4.content.res.ResourcesCompat
+import android.support.v7.widget.*
+import android.support.v7.widget.PopupMenu
+import android.util.SparseBooleanArray
+import android.view.*
+import android.widget.*
+import ml.adamsprogs.bimba.R
+import android.view.LayoutInflater
+import kotlinx.coroutines.experimental.CommonPool
+import kotlinx.coroutines.experimental.android.UI
+import kotlinx.coroutines.experimental.async
+import kotlinx.coroutines.experimental.launch
+import java.util.*
+import ml.adamsprogs.bimba.Declinator
+import ml.adamsprogs.bimba.collections.FavouriteStorage
+import ml.adamsprogs.bimba.secondsAfterMidnight
+
+
+class FavouritesAdapter(val appContext: Context, var favourites: FavouriteStorage,
+                        private val onMenuItemClickListener: OnMenuItemClickListener,
+                        private val onClickListener: ViewHolder.OnClickListener) :
+        RecyclerView.Adapter<FavouritesAdapter.ViewHolder>() {
+
+    private val selectedItems = SparseBooleanArray()
+
+    private fun isSelected(position: Int) = getSelectedItems().contains(position)
+
+    fun toggleSelection(position: Int) {
+        if (selectedItems.get(position, false)) {
+            selectedItems.delete(position)
+        } else {
+            selectedItems.put(position, true)
+        }
+        notifyItemChanged(position)
+    }
+
+    fun clearSelection() {
+        val selection = getSelectedItems()
+        selectedItems.clear()
+        for (i in selection) {
+            notifyItemChanged(i)
+        }
+    }
+
+    fun getSelectedItemCount() = selectedItems.size()
+
+    fun getSelectedItems(): List<Int> {
+        val items = ArrayList<Int>(selectedItems.size())
+        (0 until selectedItems.size()).mapTo(items) { selectedItems.keyAt(it) }
+        return items
+    }
+
+    override fun getItemCount() = favourites.size
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        launch(UI) {
+            val favourite = favourites[position]!!
+            holder.nameTextView.text = favourite.name
+
+            holder.selectedOverlay.visibility = if (isSelected(position)) View.VISIBLE else View.INVISIBLE
+            holder.moreButton.setOnClickListener {
+                val popup = PopupMenu(appContext, it)
+                val inflater = popup.menuInflater
+                popup.setOnMenuItemClickListener {
+                    when (it.itemId) {
+                        R.id.favourite_edit -> onMenuItemClickListener.edit(favourite.name)
+                        R.id.favourite_delete -> onMenuItemClickListener.delete(favourite.name)
+                        else -> false
+                    }
+                }
+                inflater.inflate(R.menu.favourite_actions, popup.menu)
+                popup.show()
+            }
+
+            val nextDeparture = async(CommonPool) {
+                favourite.nextDeparture()
+            }.await()
+
+            val nextDepartureText: String
+            val nextDepartureLineText: String
+            if (nextDeparture != null) {
+                val interval = nextDeparture.timeTill(Calendar.getInstance().secondsAfterMidnight())
+                nextDepartureLineText = appContext.getString(R.string.departure_to_line, nextDeparture.line, nextDeparture.headsign)
+                nextDepartureText = if (interval < 0)
+                    appContext.getString(R.string.just_departed)
+                else
+                    appContext.getString(Declinator.decline(interval), interval.toString())
+            } else {
+                nextDepartureText = appContext.getString(R.string.no_next_departure)
+                nextDepartureLineText = ""
+            }
+            holder.timeTextView.text = nextDepartureText
+            holder.lineTextView.text = nextDepartureLineText
+            if (nextDeparture != null) {
+                if (nextDeparture.vm)
+                    holder.typeIcon.setImageDrawable(ResourcesCompat.getDrawable(appContext.resources, R.drawable.ic_departure_vm, appContext.theme))
+                else
+                    holder.typeIcon.setImageDrawable(ResourcesCompat.getDrawable(appContext.resources, R.drawable.ic_departure_timetable, appContext.theme))
+            }
+        }
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        val context = parent.context
+        val inflater = LayoutInflater.from(context)
+
+        val rowView = inflater.inflate(R.layout.row_favourite, parent, false)
+        return ViewHolder(rowView, onClickListener)
+    }
+
+    class ViewHolder(itemView: View, private val listener: OnClickListener) : RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener {
+        override fun onLongClick(v: View?): Boolean {
+            return listener.onItemLongClicked(adapterPosition)
+        }
+
+        override fun onClick(v: View?) {
+            listener.onItemClicked(adapterPosition)
+        }
+
+        val selectedOverlay: View = itemView.findViewById(R.id.selected_overlay)
+        val nameTextView: TextView = itemView.findViewById(R.id.favourite_name)
+        val timeTextView: TextView = itemView.findViewById(R.id.favourite_time)
+        val lineTextView: TextView = itemView.findViewById(R.id.favourite_line)
+        val moreButton: ImageView = itemView.findViewById(R.id.favourite_more_button)
+        val typeIcon: ImageView = itemView.findViewById(R.id.departureTypeIcon)
+
+        init {
+            itemView.setOnClickListener(this)
+            itemView.setOnLongClickListener(this)
+        }
+
+        interface OnClickListener {
+            fun onItemClicked(position: Int)
+            fun onItemLongClicked(position: Int): Boolean
+        }
+    }
+
+    interface OnMenuItemClickListener {
+        fun edit(name: String): Boolean
+        fun delete(name: String): Boolean
+    }
+}
\ No newline at end of file




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 42ffd2a2b02df0482f2b00767b902ea66d809e34..6b7c47a92bd4100cf4aa28d34f91a7987cb34c66 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -15,6 +15,7 @@     no database background
     <string name="no_db_connect">Connect to the Internet to download the timetable</string>
     <string name="no_db_downloading">Timetable is being downloaded…</string>
     <string name="timetable_downloading">Downloading timetable</string>
+    <string name="timetable_uncompressing">%1$d/%2$d, uncompressing: %3$f%</string>
     <string name="search_placeholder">Stop or line…</string>
     <string name="no_connectivity">No connectivity – can’t update timetable</string>
     <string name="timetable_up_to_date">Timetable is up-to-date</string>
@@ -71,7 +72,6 @@     Valid till %1$s
     <string name="departure_floor" translatable="false">departure floor type (lowFloor)</string>
     <string name="departure_info" translatable="false">departure info icon</string>
     <string name="refreshing_cache">Refreshing cache. May take some time…</string>
-    <string name="timetable_converting">Converting timetable…</string>
 
     <string name="today">Today</string>
     <string name="no_departures">No departures</string>




diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index e9534bf4467d28c55290d78ef6b12726781c273c..744bb00f8113915c6bcdf0bd600b32e4f788bb62 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -59,7 +59,6 @@         "heute zeigen (wenn es Dienstag ist, es wird auf „Arbeitstage“ Karte sein).\n"
         "Sei sicher, die Nachrichten auf\nhttps://www.ztm.poznan.pl/en\nkonsultieren.\n\n"
     </string>
     <string name="refreshing_cache">Cache wird aktualisiert. Es kann einige Zeit dauern…</string>
-    <string name="timetable_converting">Fahrplan wird umgewandelt…</string>
     <string name="today">Heute</string>
     <string name="no_departures">Keine Abfahrten</string>
     <string name="tab_text_line_to">Hin</string>
@@ -68,4 +67,5 @@     Fahrplan gilt nur bis heute.
     <string name="timetable_validity_finished">Die Gültigkeit des Zeitplans ist beendet. Verbind mit dem Internet, um eine neue herunterzuladen und um fortzufahren.</string>
     <string name="timetable_validity_tomorrow">Fahrplan gilt nur bis morgen.</string>
     <string name="just_departed">Gerade gegangen</string>
+    <string name="timetable_uncompressing">%1$d/%2$d, dekompression: %3$f%</string>
 </resources>




diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index dff70c86c95fb00f7f973232d2766e713e55a31c..d71472cece3d9dc77c59bf6043f2e5fec0f45f4c 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -57,7 +57,6 @@         "(se è martedì, sarà nella scheda «giorni di lavoro»).\n"
         "Assicurati di consultare le notifiche su\nhttps://www.ztm.poznan.pl/en.\n\n"
     </string>
     <string name="refreshing_cache">Cache sta essendo aggiornato. Può richiedere un certo tempo…</string>
-    <string name="timetable_converting">L’orario e stando convertito</string>
     <string name="today">Oggi</string>
     <string name="no_departures">Nessune partenze</string>
     <string name="tab_text_line_to">Avanti</string>
@@ -66,4 +65,5 @@     L’orario è valido solo fino ad oggi.
     <string name="timetable_validity_finished">"La validità dell’orario è terminata. Connetti a Internet per scaricarne uno nuovo e continuare. "</string>
     <string name="timetable_validity_tomorrow">L’orario è valido solo fino a domani.</string>
     <string name="just_departed">Appena partito</string>
+    <string name="timetable_uncompressing">%1$d/%2$d, decompressione: %3$f%</string>
 </resources>




diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 2871fa773db94ca328522d1a8b71a9e06e5c3921..90ab004f7fb7dbb77177895c181e8e4f29e7fe35 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -59,7 +59,6 @@     Zbieranie odjazdów…
     <string name="valid_since">Ważny od %1$s</string>
     <string name="valid_till">Ważny do %1$s</string>
     <string name="refreshing_cache">Odświeżanie pamięci podręcznej. Może chwilę potrwać…</string>
-    <string name="timetable_converting">Konwertowanie rozkładu</string>
     <string name="today">Dzisiaj</string>
     <string name="no_departures">Brak odjazdów</string>
     <string name="tab_text_line_to">Tam</string>
@@ -68,4 +67,5 @@     Rozkład obowiązuje tylko do dzisiaj.
     <string name="timetable_validity_finished">Rozkład przestał obowiązywać. Połącz się z Internetem, aby pobrać nowy i kontynuować.</string>
     <string name="timetable_validity_tomorrow">Rozkład obowiązuje tylko do jutra.</string>
     <string name="just_departed">Właśnie odjechał</string>
+    <string name="timetable_uncompressing">%1$d/%2$d, dekompresja: %3$f%</string>
 </resources>
\ No newline at end of file




diff --git a/build.gradle b/build.gradle
index e67481b9243b782c24e7bbe927900e7d970012f6..f7dc8977568630d236b494ed8f2f4f6e2b9efcd2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -9,7 +9,7 @@         //maven { url 'https://dl.bintray.com/guardian/android' } // TooLargeTool
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.0.1'
+        classpath 'com.android.tools.build:gradle:3.1.0'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 
         // NOTE: Do not place your application dependencies here; they belong