Bimba.git

commit f74fa5279541934876925766e84ac33919784252

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

Merge branch 'schedulelessness' into develop

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


diff --git a/app/build.gradle b/app/build.gradle
index ceec2719de9cdc6a9df162cb04300385ca2912b3..50d4623bad91c9ccaeac333cac6c4e2a930a7e6e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -4,9 +4,9 @@ apply plugin: 'kotlin-android-extensions'
 
 android {
     compileSdkVersion 27
-    buildToolsVersion "28.0.1"
+    buildToolsVersion "28.0.2"
     defaultConfig {
-        applicationId "ml.adamsprogs.bimba"
+        applicationId "ml.adamsprogs.bimba.scheduleless"
         minSdkVersion 19
         targetSdkVersion 27
         versionCode 14




diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5f4681fca49b83a45a0610ee2e176da39900d4ec..58ac9e4d29069e199be5ffbd848d746b75ee26e6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,7 +8,7 @@     
 
     <application
         android:allowBackup="true"
-        android:icon="@mipmap/ic_launcher"
+        android:icon="@drawable/icon_dev"
         android:label="@string/app_name"
         android:supportsRtl="true"
         android:theme="@style/AppTheme">
@@ -27,7 +27,6 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-        <activity android:name=".activities.NoDbActivity" />
         <activity android:name=".activities.EditFavouriteActivity" />
         <activity
             android:name=".activities.SettingsActivity"
@@ -44,7 +43,7 @@             android:label="@string/title_activity_help"
             android:theme="@style/AppTheme" />
 
         <service
-            android:name=".datasources.VmClient"
+            android:name=".datasources.VmService"
             android:enabled="true"
             android:exported="false" />
 




diff --git a/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt b/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt
index 9668aa001bce218cf2f16f22b1e70a19b4a8ff75..c40abec7cc2d1edd1e714a8a4f24dbe6afb6d577 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt
@@ -4,7 +4,7 @@ import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
 import ml.adamsprogs.bimba.datasources.TimetableDownloader
-import ml.adamsprogs.bimba.datasources.VmClient
+import ml.adamsprogs.bimba.datasources.VmService
 import ml.adamsprogs.bimba.models.Departure
 import ml.adamsprogs.bimba.models.Plate
 
@@ -28,11 +28,12 @@             for (listener in onTimetableDownloadListeners) {
                 listener.onTimetableDownload(result)
             }
         }
-        if (intent?.action == VmClient.ACTION_READY) {
-            val departures = intent.getStringArrayListExtra(VmClient.EXTRA_DEPARTURES)?.map { Departure.fromString(it) }?.toSet()
-            val plateId = intent.getSerializableExtra(VmClient.EXTRA_PLATE_ID) as Plate.ID
+        if (intent?.action == VmService.ACTION_READY) {
+            val departures = intent.getStringArrayListExtra(VmService.EXTRA_DEPARTURES)?.map { Departure.fromString(it) }?.toSet()
+            val plateId = intent.getSerializableExtra(VmService.EXTRA_PLATE_ID) as Plate.ID?
+            val stopCode = intent.getSerializableExtra(VmService.EXTRA_STOP_CODE) as String
             for (listener in onVmListeners) {
-                listener.onVm(departures, plateId)
+                listener.onVm(departures, plateId, stopCode)
             }
         }
     }
@@ -58,6 +59,6 @@         fun onTimetableDownload(result: String?)
     }
 
     interface OnVmListener {
-        fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID)
+        fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID?, stopCode: String)
     }
 }
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/NetworkStateReceiver.kt b/app/src/main/java/ml/adamsprogs/bimba/NetworkStateReceiver.kt
index 70a3a1652bc188fb666fd3841247a52422388b1c..2acd1e1eb9dbb95db986e6907c6e07db549c9792 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/NetworkStateReceiver.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/NetworkStateReceiver.kt
@@ -37,9 +37,14 @@         fun onConnectivityChange(connected: Boolean)
     }
 
     companion object {
-        fun isNetworkAvailable(context: Context): Boolean {
-            val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
-            val activeNetworkInfo = connectivityManager.activeNetworkInfo
+        lateinit var manager: ConnectivityManager
+
+        fun init(context: Context) {
+            manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+        }
+
+        fun isNetworkAvailable(): Boolean {
+            val activeNetworkInfo = manager.activeNetworkInfo
             return activeNetworkInfo != null && activeNetworkInfo.isConnected
         }
     }




diff --git a/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt b/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt
new file mode 100644
index 0000000000000000000000000000000000000000..32dddcd358b1801b29be9440b9a0c0322cdb8460
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt
@@ -0,0 +1,255 @@
+package ml.adamsprogs.bimba
+
+import android.content.*
+import kotlinx.coroutines.experimental.android.UI
+import kotlinx.coroutines.experimental.*
+import ml.adamsprogs.bimba.datasources.*
+import ml.adamsprogs.bimba.models.*
+import ml.adamsprogs.bimba.models.suggestions.*
+import java.util.*
+import kotlin.collections.HashMap
+
+//todo make singleton
+class ProviderProxy(context: Context? = null) {
+    private val vmClient = VmClient.getVmClient()
+    private var timetable: Timetable = Timetable.getTimetable(context)
+    private var suggestions = emptyList<GtfsSuggestion>()
+    private val requests = HashMap<String, Request>()
+
+    var mode = if (timetable.isEmpty()) MODE_VM else MODE_FULL
+
+    companion object {
+        const val MODE_FULL = "mode_full"
+        const val MODE_VM = "mode_vm"
+    }
+
+    fun getSuggestions(query: String = "", callback: (List<GtfsSuggestion>) -> Unit) {
+        launch(UI) {
+            val filtered = withContext(DefaultDispatcher) {
+                suggestions = getStopSuggestions(query) //+ getLineSuggestions(query) //todo<p:v+1> + bike stations, train stations, &c
+                filterSuggestions(query)
+            }
+            callback(filtered)
+        }
+    }
+
+    private suspend fun getStopSuggestions(query: String): List<StopSuggestion> {
+        val vmSuggestions = withContext(DefaultDispatcher) {
+            vmClient.getStops(query)
+        }
+
+        return if (vmSuggestions.isEmpty() and !timetable.isEmpty()) {
+            timetable.getStopSuggestions()
+        } else {
+            vmSuggestions
+        }
+    }
+
+    private fun filterSuggestions(query: String): List<GtfsSuggestion> {
+        return suggestions.filter {
+            deAccent(it.name).contains(deAccent(query), true)
+        }
+    }
+
+    private fun deAccent(str: String): String {
+        var result = str.replace('ę', 'e', true)
+        result = result.replace('ó', 'o', true)
+        result = result.replace('ą', 'a', true)
+        result = result.replace('ś', 's', true)
+        result = result.replace('ł', 'l', true)
+        result = result.replace('ż', 'z', true)
+        result = result.replace('ź', 'z', true)
+        result = result.replace('ć', 'c', true)
+        result = result.replace('ń', 'n', true)
+        return result
+    }
+
+    fun getSheds(name: String, callback: (Map<String, Set<String>>) -> Unit) {
+        launch(UI) {
+            val sheds = withContext(DefaultDispatcher) {
+                val vmSheds = vmClient.getSheds(name)
+
+                if (vmSheds.isEmpty() and !timetable.isEmpty()) {
+                    timetable.getHeadlinesForStop(name)
+                } else {
+                    vmSheds
+                }
+            }
+
+            callback(sheds)
+        }
+    }
+
+    fun subscribeForDepartures(stopSegments: Set<StopSegment>, listener: OnDeparturesReadyListener, context: Context): String {
+        stopSegments.forEach {
+            val intent = Intent(context, VmService::class.java)
+            intent.putExtra("stop", it.stop)
+            intent.action = "request"
+            context.startService(intent)
+        }
+        val uuid = UUID.randomUUID().toString()
+        requests[uuid] = Request(listener, stopSegments)
+        return uuid
+    }
+
+    fun subscribeForDepartures(stopCode: String, listener: OnDeparturesReadyListener, context: Context): String {
+        val intent = Intent(context, VmService::class.java)
+        intent.putExtra("stop", stopCode)
+        intent.action = "request"
+        context.startService(intent)
+
+        val uuid = UUID.randomUUID().toString()
+        requests[uuid] = Request(listener, setOf(StopSegment(stopCode, null)))
+        return uuid
+    }
+
+    private fun constructSegmentDepartures(stopSegments: Set<StopSegment>): Deferred<Map<String, List<Departure>>> {
+        return async {
+            if (timetable.isEmpty())
+                emptyMap()
+            else {
+                timetable.getStopDeparturesBySegments(stopSegments)
+            }
+        }
+    }
+
+    private fun filterDepartures(departures: Map<String, List<Departure>>): List<Departure> {
+        val now = Calendar.getInstance().secondsAfterMidnight()
+        val lines = HashMap<String, Int>()
+        val twoDayDepartures = (timetable.getServiceForToday()?.let {
+            departures[it]
+        } ?: emptyList()) +
+                (timetable.getServiceForTomorrow()?.let { service ->
+                    departures[service]!!.map { it.copy().apply { tomorrow = true } }
+                } ?: emptyList())
+
+        return twoDayDepartures
+                .filter { it.timeTill(now) >= 0 }
+                .filter {
+                    val existed = lines[it.line] ?: 0
+                    if (existed < 3) {
+                        lines[it.line] = existed + 1
+                        true
+                    } else false
+                }
+    }
+
+    fun unsubscribeFromDepartures(uuid: String, context: Context) {
+        requests[uuid]?.unsubscribe(context)
+        requests.remove(uuid)
+    }
+
+    fun refreshTimetable(context: Context) {
+        timetable = Timetable.getTimetable(context, true)
+        mode = MODE_FULL
+    }
+
+    fun getFullTimetable(stopCode: String): Map<String, List<Departure>> {
+        return if (timetable.isEmpty())
+            emptyMap()
+        else
+            timetable.getStopDepartures(stopCode)
+
+    }
+
+    fun getFullTimetable(stopSegments: Set<StopSegment>): Map<String, List<Departure>> {
+        return if (timetable.isEmpty())
+            emptyMap()
+        else
+            timetable.getStopDeparturesBySegments(stopSegments)
+
+    }
+
+    fun fillStopSegment(stopSegment: StopSegment, callback: (StopSegment?) -> Unit) {
+        launch(UI) {
+            withContext(DefaultDispatcher) {
+                callback(fillStopSegment(stopSegment))
+            }
+        }
+    }
+
+    suspend fun fillStopSegment(stopSegment: StopSegment): StopSegment? {
+        if (stopSegment.plates != null)
+            return stopSegment
+
+        return if (timetable.isEmpty())
+            vmClient.getDirections(stopSegment.stop)
+        else
+            timetable.getHeadlinesForStopCode(stopSegment.stop)
+    }
+
+    fun getStopName(stopCode: String, callback: (String?) -> Unit) {
+        launch(UI) {
+            withContext(DefaultDispatcher) {
+                callback(getStopName(stopCode))
+            }
+        }
+    }
+
+    suspend fun getStopName(stopCode: String): String? {
+        return if (timetable.isEmpty())
+            vmClient.getName(stopCode)
+        else
+            timetable.getStopName(stopCode)
+    }
+
+    fun describeService(service: String, context: Context): String? {
+        return if (timetable.isEmpty())
+            null
+        else
+            timetable.getServiceDescription(service, context)
+    }
+
+    fun getServiceFirstDay(service: String): Int {
+        return timetable.getServiceFirstDay(service)
+    }
+
+    interface OnDeparturesReadyListener {
+        fun onDeparturesReady(departures: List<Departure>, plateId: Plate.ID?)
+    }
+
+    inner class Request(private val listener: OnDeparturesReadyListener, private val segments: Set<StopSegment>) : MessageReceiver.OnVmListener {
+        private val receiver = MessageReceiver.getMessageReceiver()
+        private val receivedPlates = HashSet<Plate.ID>()
+
+        private var cache: Deferred<Map<String, List<Departure>>>? = null
+
+        init {
+            receiver.addOnVmListener(this@Request)
+            launch(UI) {
+                cache = constructSegmentDepartures(segments)
+            }
+        }
+
+        override fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID?, stopCode: String) {
+            launch(UI) {
+                if (plateId == null) {
+                    listener.onDeparturesReady(filterDepartures(cache!!.await()), null)
+                } else {
+                    if (segments.any { plateId in it }) {
+                        if (vmDepartures != null) {
+                            listener.onDeparturesReady(vmDepartures.toList(), plateId)
+                            if (plateId !in receivedPlates)
+                                receivedPlates.add(plateId)
+                        } else {
+                            receivedPlates.remove(plateId)
+                            if (receivedPlates.isEmpty()) {
+                                listener.onDeparturesReady(filterDepartures(cache!!.await()), null)
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        fun unsubscribe(context: Context) {
+            segments.forEach {
+                val intent = Intent(context, VmService::class.java)
+                intent.putExtra("stop", it.stop)
+                intent.action = "remove"
+                context.startService(intent)
+            }
+            receiver.removeOnVmListener(this)
+        }
+    }
+}
\ No newline at end of file




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 b3389a44b0e630c67900e4fd810e80964521eae8..a3bcc6e7dc5caf1b31a1d20b76a1062f914329b0 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
@@ -3,8 +3,8 @@
 import android.annotation.SuppressLint
 import android.app.Activity
 import android.content.*
-import android.database.sqlite.SQLiteException
 import android.os.*
+import android.preference.PreferenceManager.getDefaultSharedPreferences
 import android.support.design.widget.*
 import android.support.v4.widget.*
 import android.support.v7.widget.*
@@ -12,7 +12,6 @@ import android.support.v7.app.*
 import android.text.Html
 import android.view.*
 import android.view.inputmethod.InputMethodManager
-import kotlin.concurrent.thread
 import kotlin.collections.ArrayList
 import kotlinx.android.synthetic.main.activity_dash.*
 import java.util.*
@@ -30,11 +29,11 @@ import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
 
 //todo<p:1> searchView integration
 class DashActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener,
-        FavouritesAdapter.OnMenuItemClickListener, Favourite.OnVmPreparedListener,
-        FavouritesAdapter.ViewHolder.OnClickListener {
+        FavouritesAdapter.OnMenuItemClickListener, FavouritesAdapter.ViewHolder.OnClickListener, ProviderProxy.OnDeparturesReadyListener {
+
     val context: Context = this
     private val receiver = MessageReceiver.getMessageReceiver()
-    var timetable: Timetable? = null
+    private lateinit var timetable: Timetable
     private var suggestions: List<GtfsSuggestion>? = null
     private lateinit var drawerLayout: DrawerLayout
     private lateinit var drawerView: NavigationView
@@ -45,6 +44,7 @@     private lateinit var adapter: FavouritesAdapter
     private val actionModeCallback = ActionModeCallback()
     private var actionMode: ActionMode? = null
     private var isWarned = false
+    private lateinit var providerProxy: ProviderProxy
 
     companion object {
         const val REQUEST_EDIT_FAVOURITE = 1
@@ -56,11 +56,9 @@         setContentView(R.layout.activity_dash)
 
         setSupportActionBar(toolbar)
 
-        timetable = try {
-            Timetable.getTimetable(this)
-        } catch (e: SQLiteException) {
-            null
-        }
+        providerProxy = ProviderProxy(this)
+        timetable = Timetable.getTimetable()
+        NetworkStateReceiver.init(this)
 
         getSuggestions()
 
@@ -77,7 +75,7 @@         //drawer.setCheckedItem(R.id.drawer_home)
         drawerView.setNavigationItemSelectedListener { item ->
             when (item.itemId) {
                 R.id.drawer_refresh -> {
-                    startDownloaderService()
+                    startDownloaderService(true)
                 }
                 R.id.drawer_help -> {
                     startActivity(Intent(context, HelpActivity::class.java))
@@ -99,7 +97,9 @@
         searchView.setOnFocusChangeListener(object : FloatingSearchView.OnFocusChangeListener {
             override fun onFocus() {
                 favouritesList.visibility = View.GONE
-                filterSuggestions(searchView.query)
+                providerProxy.getSuggestions(searchView.query) {
+                    searchView.swapSuggestions(it)
+                }
             }
 
             override fun onFocusCleared() {
@@ -110,7 +110,9 @@
         searchView.setOnQueryChangeListener { oldQuery, newQuery ->
             if (oldQuery != "" && newQuery == "")
                 searchView.clearSuggestions()
-            filterSuggestions(newQuery)
+            providerProxy.getSuggestions(newQuery) {
+                searchView.swapSuggestions(it)
+            }
         }
 
         searchView.setOnSearchListener(object : FloatingSearchView.OnSearchListener {
@@ -123,7 +125,6 @@                 }
                 imm.hideSoftInputFromWindow(view.windowToken, 0)
                 if (searchSuggestion is StopSuggestion) {
                     val intent = Intent(context, StopSpecifyActivity::class.java)
-                    intent.putExtra(StopSpecifyActivity.EXTRA_STOP_IDS, searchSuggestion.ids.joinToString(",") { it.id })
                     intent.putExtra(StopSpecifyActivity.EXTRA_STOP_NAME, searchSuggestion.name)
                     startActivity(intent)
                 } else if (searchSuggestion is LineSuggestion) {
@@ -151,41 +152,49 @@
         searchView.attachNavigationDrawerToMenuButton(drawer_layout as DrawerLayout)
     }
 
+    override fun onRestart() {
+        super.onRestart()
+        favourites = FavouriteStorage.getFavouriteStorage(context)
+        favourites.forEach {
+            it.subscribeForDepartures(this, this)
+        }
+    }
+
+    override fun onStop() {
+        super.onStop()
+        favourites.forEach {
+            it.unsubscribeFromDepartures(this)
+        }
+    }
+
     private fun showValidityInDrawer() {
-        if (timetable == null) {
+        if (timetable.isEmpty()) {
             drawerView.menu.findItem(R.id.drawer_validity_since).title = getString(R.string.validity_offline_unavailable)
         } else {
             val formatter = DateFormat.getDateInstance(DateFormat.SHORT)
-            var calendar = calendarFromIsoD(timetable!!.getValidSince())
+            var calendar = calendarFromIsoD(timetable.getValidSince())
             formatter.timeZone = calendar.timeZone
             drawerView.menu.findItem(R.id.drawer_validity_since).title = getString(R.string.valid_since, formatter.format(calendar.time))
-            calendar = calendarFromIsoD(timetable!!.getValidTill())
+            calendar = calendarFromIsoD(timetable.getValidTill())
             formatter.timeZone = calendar.timeZone
             drawerView.menu.findItem(R.id.drawer_validity_till).title = getString(R.string.valid_till, formatter.format(calendar.time))
         }
     }
 
-    private fun filterSuggestions(newQuery: String) {
-        thread {
-            val newStops = suggestions!!.filter { deAccent(it.name).contains(deAccent(newQuery), true) } //todo<p:2> sorted by similarity
-            runOnUiThread { searchView.swapSuggestions(newStops) }
-        }
-    }
-
     private fun warnTimetableValidity() {
         if (isWarned)
             return
         isWarned = true
-        if (timetable == null)
+        if (timetable.isEmpty())
             return
-        val validTill = timetable!!.getValidTill()
+        val validTill = timetable.getValidTill()
         val today = Calendar.getInstance().toIsoDate()
         val tomorrow = Calendar.getInstance().apply {
             this.add(Calendar.DAY_OF_MONTH, 1)
         }.toIsoDate()
 
         try {
-            timetable!!.getServiceForToday()
+            timetable.getServiceForToday()
             if (today > validTill) {
                 notifyTimetableValidity(-1)
                 suggestions = ArrayList()
@@ -202,7 +211,7 @@             return
         }
 
         try {
-            timetable!!.getServiceForTomorrow()
+            timetable.getServiceForTomorrow()
             if (tomorrow == validTill) {
                 notifyTimetableValidity(1)
                 return
@@ -226,12 +235,14 @@                 ) { dialog: DialogInterface, _: Int -> dialog.cancel() }
                 .setCancelable(true)
                 .setMessage(message)
                 .create().show()
+
+        //todo if days == -1 -> delete timetable
     }
 
     private fun prepareFavourites() {
         favourites = FavouriteStorage.getFavouriteStorage(context)
         favourites.forEach {
-            it.addOnVmPreparedListener(this)
+            it.subscribeForDepartures(this, this)
         }
         val layoutManager = LinearLayoutManager(context)
         favouritesList = favourites_list
@@ -241,31 +252,30 @@         favouritesList.itemAnimator = DefaultItemAnimator()
         favouritesList.layoutManager = layoutManager
     }
 
-    override fun onVmPrepared() {
+    override fun onDeparturesReady(departures: List<Departure>, plateId: Plate.ID?) {
         favouritesList.adapter.notifyDataSetChanged()
     }
 
     private fun getSuggestions() {
-        suggestions = if (timetable != null)
-            (timetable!!.getStopSuggestions(context)).sorted() //+ timetable.getLineSuggestions()).sorted() //todo<p:v+1> + bike stations, train stations, &c
-        else
-            emptyList()
+        providerProxy.getSuggestions {
+            searchView.swapSuggestions(it)
+        }
     }
 
     private fun prepareListeners() {
         val filter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED)
-        filter.addAction(VmClient.ACTION_READY)
+        filter.addAction(VmService.ACTION_READY)
         filter.addCategory(Intent.CATEGORY_DEFAULT)
         registerReceiver(receiver, filter)
         receiver.addOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener)
-        favourites.registerOnVm(receiver, context)
     }
 
-    private fun startDownloaderService() {
-        startService(Intent(context, TimetableDownloader::class.java))
+    private fun startDownloaderService(force: Boolean = false) {
+        if (getDefaultSharedPreferences(this).getBoolean("automatic timetable updates", false) or force)
+            startService(Intent(context, TimetableDownloader::class.java))
     }
 
-    override fun onBackPressed() {
+    override fun onBackPressed() { //fixme
         if (drawerLayout.isDrawerOpen(drawerView)) {
             drawerLayout.closeDrawer(drawerView)
             return
@@ -284,21 +294,7 @@
     override fun onDestroy() {
         super.onDestroy()
         receiver.removeOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener)
-        favourites.deregisterOnVm(receiver, context)
         unregisterReceiver(receiver)
-    }
-
-    private fun deAccent(str: String): String {
-        var result = str.replace('ę', 'e', true)
-        result = result.replace('ó', 'o', true)
-        result = result.replace('ą', 'a', true)
-        result = result.replace('ś', 's', true)
-        result = result.replace('ł', 'l', true)
-        result = result.replace('ż', 'z', true)
-        result = result.replace('ź', 'z', true)
-        result = result.replace('ć', 'c', true)
-        result = result.replace('ń', 'n', true)
-        return result
     }
 
     override fun onTimetableDownload(result: String?) {




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/LineSpecifyActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/LineSpecifyActivity.kt
index 5d7d949f83b9112fd606741d84d156d57990cd8a..e33d1edd5cf5253071bf0936ca77d35211ce2426 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/LineSpecifyActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/LineSpecifyActivity.kt
@@ -30,7 +30,7 @@
         val line = intent.getStringExtra(EXTRA_LINE_ID)
 
         val timetable = Timetable.getTimetable()
-        val graphs = timetable.getTripGraphs(AgencyAndId(line))
+        val graphs = timetable.getTripGraphs(line)
 
         sectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager, graphs)
 




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
deleted file mode 100644
index 12157af53cd4f527780038c4c7495da4cb8c1c92..0000000000000000000000000000000000000000
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-package ml.adamsprogs.bimba.activities
-
-import android.content.Context
-import android.content.Intent
-import android.support.v7.app.AppCompatActivity
-import android.os.Bundle
-import android.content.IntentFilter
-import ml.adamsprogs.bimba.*
-import kotlinx.android.synthetic.main.activity_nodb.*
-import ml.adamsprogs.bimba.datasources.TimetableDownloader
-import ml.adamsprogs.bimba.models.Timetable
-
-class NoDbActivity : AppCompatActivity(), NetworkStateReceiver.OnConnectivityChangeListener, MessageReceiver.OnTimetableDownloadListener {
-    private val networkStateReceiver = NetworkStateReceiver()
-    private val timetableDownloadReceiver = MessageReceiver.getMessageReceiver()
-    private var serviceRunning = false
-    private var askedForNetwork = false
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_nodb)
-        val editor = getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE).edit()
-        editor.putString("etag", "")
-        editor.apply()
-
-        var filter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED)
-        filter.addCategory(Intent.CATEGORY_DEFAULT)
-        registerReceiver(timetableDownloadReceiver, filter)
-        timetableDownloadReceiver.addOnTimetableDownloadListener(this)
-
-        if (!NetworkStateReceiver.isNetworkAvailable(this)) {
-            askedForNetwork = true
-            no_db_caption.text = getString(R.string.no_db_connect)
-            filter = IntentFilter("android.net.conn.CONNECTIVITY_CHANGE")
-            registerReceiver(networkStateReceiver, filter)
-            networkStateReceiver.addOnConnectivityChangeListener(this)
-        } else
-            downloadTimetable()
-
-        skip_button.setOnClickListener {
-            /*
-            val editor = getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE).edit()
-            editor.putBoolean(Timetable.ONLY_ONLINE, true)
-            editor.apply()*/
-            startActivity(Intent(this, DashActivity::class.java))
-            finish()
-        }
-    }
-
-    override fun onResume() {
-        super.onResume()
-        try {
-            val timetable = Timetable.getTimetable(this, true)
-            if (!timetable.isEmpty()) {
-                startActivity(Intent(this, DashActivity::class.java))
-                finish()
-            }
-        } catch (e:Exception){}
-        var filter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED)
-        filter.addCategory(Intent.CATEGORY_DEFAULT)
-        registerReceiver(timetableDownloadReceiver, filter)
-        if (!NetworkStateReceiver.isNetworkAvailable(this)) {
-            askedForNetwork = true
-            no_db_caption.text = getString(R.string.no_db_connect)
-            filter = IntentFilter("android.net.conn.CONNECTIVITY_CHANGE")
-            registerReceiver(networkStateReceiver, filter)
-            networkStateReceiver.addOnConnectivityChangeListener(this)
-        } else if (!serviceRunning)
-            downloadTimetable()
-    }
-
-    private fun downloadTimetable() {
-        no_db_caption.text = getString(R.string.no_db_downloading)
-        serviceRunning = true
-        intent = Intent(this, TimetableDownloader::class.java)
-        intent.putExtra(TimetableDownloader.EXTRA_FORCE, true)
-        startService(intent)
-    }
-
-    override fun onConnectivityChange(connected: Boolean) {
-        if (connected && !serviceRunning)
-            downloadTimetable()
-        /*if (!connected)
-            serviceRunning = false*/
-    }
-
-    override fun onTimetableDownload(result: String?) {
-        when (result) {
-            TimetableDownloader.RESULT_FINISHED -> {
-                timetableDownloadReceiver.removeOnTimetableDownloadListener(this)
-                networkStateReceiver.removeOnConnectivityChangeListener(this)
-                startActivity(Intent(this, DashActivity::class.java))
-                finish()
-            }
-            else -> no_db_caption.text = getString(R.string.error_try_later)
-        }
-    }
-
-    override fun onPause() {
-        super.onPause()
-        unregisterReceiver(timetableDownloadReceiver)
-        if (askedForNetwork)
-            unregisterReceiver(networkStateReceiver)
-    }
-}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt
index 5a2d486aa09d33eae44c5d3d1686830a8adcbb2d..e7c3e0085a8ad06f180703cd533e0848579b9752 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt
@@ -14,15 +14,7 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO)
-        try {
-            val timetable = Timetable.getTimetable(this)
-            if (timetable.isEmpty())
-                startActivity(Intent(this, NoDbActivity::class.java))
-            else
-                startActivity(Intent(this, DashActivity::class.java))
-        } catch (e:Exception) {
-            startActivity(Intent(this, NoDbActivity::class.java))
-        }
+        startActivity(Intent(this, DashActivity::class.java))
         finish()
     }
 }




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 a369cc81ddece00c5313ef0e19482d13785f74a1..b6dbb3eeab4dbf3ca1b8ef2bd3077c7276377fd9 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
@@ -4,27 +4,24 @@ import android.content.*
 import android.support.design.widget.*
 import android.os.Bundle
 import android.view.*
-import android.support.v4.app.*
-import android.support.v4.view.PagerAdapter
 import android.support.v4.content.res.ResourcesCompat
 import android.support.v7.app.AppCompatActivity
 import android.support.v7.widget.*
+import android.widget.AdapterView
 
 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 {
+import ml.adamsprogs.bimba.models.adapters.ServiceAdapter
 
-    private var sectionsPagerAdapter: SectionsPagerAdapter? = null
-
+class StopActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, ProviderProxy.OnDeparturesReadyListener {
     companion object {
-        const val EXTRA_STOP_ID = "stopId"
+        const val EXTRA_STOP_CODE = "stopCode"
+        const val EXTRA_STOP_NAME = "stopName"
         const val EXTRA_FAVOURITE = "favourite"
         const val SOURCE_TYPE = "sourceType"
         const val SOURCE_TYPE_STOP = "stop"
@@ -33,108 +30,79 @@
         const val MODE_WORKDAYS = 0
         const val MODE_SATURDAYS = 1
         const val MODE_SUNDAYS = 2
+
+        const val TIMETABLE_TYPE_DEPARTURE = "timetable_type_departure"
+        const val TIMETABLE_TYPE_FULL = "timetable_type_full"
     }
 
-    private var stopSegment: StopSegment? = null
+    private var stopCode = ""
     private var favourite: Favourite? = null
-    private var timetableType = "departure"
-    private lateinit var timetable: Timetable
+    private var timetableType = TIMETABLE_TYPE_DEPARTURE
     private val context = this
     private val receiver = MessageReceiver.getMessageReceiver()
-    private val vmDepartures = HashMap<Plate.ID, Set<Departure>>()
-    private var hasDepartures = false
-    private var lastUpdated = 0L
+    private lateinit var providerProxy: ProviderProxy
+    private val departures = HashMap<Plate.ID, List<Departure>>()
+    private val fullDepartures = HashMap<String, List<Departure>>()
+    private lateinit var subscriptionId: String
+    private lateinit var adapter: DeparturesAdapter
+
 
     private lateinit var sourceType: String
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_stop)
 
-        timetable = Timetable.getTimetable(this)
+        providerProxy = ProviderProxy(this)
 
         sourceType = intent.getStringExtra(SOURCE_TYPE)
 
         setSupportActionBar(toolbar)
 
-        val departures = when (sourceType) {
+        when (sourceType) {
             SOURCE_TYPE_STOP -> {
-                stopSegment = StopSegment(intent.getSerializableExtra(EXTRA_STOP_ID) as AgencyAndId, null).apply { fillPlates() }
-                supportActionBar?.title = timetable.getStopName(stopSegment!!.stop)
-                null
+                stopCode = intent.getSerializableExtra(EXTRA_STOP_CODE) as String
+                supportActionBar?.title = intent.getSerializableExtra(EXTRA_STOP_NAME) as String
             }
             SOURCE_TYPE_FAV -> {
                 favourite = intent.getParcelableExtra(EXTRA_FAVOURITE)
                 supportActionBar?.title = favourite!!.name
-                favourite!!.addOnVmPreparedListener(this)
-                if (favourite!!.fullDepartures.isNotEmpty())
-                    favourite!!.fullDepartures
-                else
-                    null
             }
-            else -> null
         }
 
         showFab()
 
-        sectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager, departures)
-
-        container.adapter = sectionsPagerAdapter
-
-        container.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs))
-        tabs.addOnTabSelectedListener(TabLayout.ViewPagerOnTabSelectedListener(container))
+        val layoutManager = LinearLayoutManager(this)
+        departuresList.addItemDecoration(DividerItemDecoration(departuresList.context, layoutManager.orientation))
+        departuresList.adapter = DeparturesAdapter(this, null, true)
+        adapter = departuresList.adapter as DeparturesAdapter
+        departuresList.layoutManager = layoutManager
 
-        selectTodayPage()
+        departuresList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {}
+            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+                updateFabVisibility(dy)
+                super.onScrolled(recyclerView, dx, dy)
+            }
+        })
 
         prepareOnDownloadListener()
-    }
-
-    private fun getFavouriteDepartures() {
-        refreshAdapter(favourite!!.allDepartures())
-    }
-
-    private fun refreshAdapterFromStop() {
-        val now = Calendar.getInstance().secondsAfterMidnight()
-        val departures = HashMap<AgencyAndId, List<Departure>>()
-        if (this.vmDepartures.isNotEmpty()) {
-            departures[timetable.getServiceForToday()] = this.vmDepartures.flatMap { it.value }.sortedBy { it.timeTill(now) }
-            refreshAdapter(departures)
-        } else {
-            refreshAdapter(Departure.createDepartures(stopSegment!!.stop))
-            hasDepartures = true
-        }
-    }
-
-    private fun refreshAdapter(departures: Map<AgencyAndId, List<Departure>>?) {
-        if (departures != null)
-            sectionsPagerAdapter?.departures = departures
-        sectionsPagerAdapter?.notifyDataSetChanged()
-        selectTodayPage()
-        lastUpdated = Calendar.getInstance().timeInMillis
-    }
-
-    override fun onVmPrepared() {
-        // println("onVmPrepared: ticked? ${ticked()}; vmBacked? ${favourite!!.isBackedByVm}")
-        if ((favourite!!.isBackedByVm || ticked()) && (timetableType == "departure")) {
-            getFavouriteDepartures()
-        }
+        subscribeForDepartures()
     }
 
     private fun showFab() {
         if (sourceType == SOURCE_TYPE_FAV)
             return
 
-        val stopSymbol = timetable.getStopCode(stopSegment!!.stop)
-
         val favourites = FavouriteStorage.getFavouriteStorage(context)
-        if (!favourites.has(stopSymbol)) {
+        if (!favourites.has(stopCode)) {
             fab.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_favourite_empty, this.theme))
         }
 
         fab.setOnClickListener {
-            if (!favourites.has(stopSymbol)) {
+            if (!favourites.has(stopCode)) {
                 val items = HashSet<StopSegment>()
-                items.add(stopSegment!!)
-                favourites.add(stopSymbol, items, this@StopActivity)
+                items.add(StopSegment(stopCode, null))
+                favourites.add(stopCode, items, this@StopActivity)
                 fab.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_favourite, this.theme))
             } else {
                 Snackbar.make(it, getString(R.string.stop_already_fav), Snackbar.LENGTH_LONG)
@@ -156,41 +124,43 @@     }
 
     private fun prepareOnDownloadListener() {
         val filter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED)
-        filter.addAction(VmClient.ACTION_READY)
+        filter.addAction(VmService.ACTION_READY)
         filter.addCategory(Intent.CATEGORY_DEFAULT)
         registerReceiver(receiver, filter)
         receiver.addOnTimetableDownloadListener(context)
-        if (sourceType == SOURCE_TYPE_STOP) {
-            receiver.addOnVmListener(context)
-            val intent = Intent(this, VmClient::class.java)
-            intent.putExtra("stop", stopSegment)
-            intent.action = "request"
-            startService(intent)
+    }
+
+    private fun subscribeForDepartures() {
+        subscriptionId = if (sourceType == SOURCE_TYPE_STOP) {
+            providerProxy.subscribeForDepartures(stopCode, this, this)
         } else
-            favourite!!.registerOnVm(receiver, context)
+            favourite!!.subscribeForDepartures(this, context)
     }
 
-    override fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID) {
-        // println("onVm")
-        if (vmDepartures == null && this.vmDepartures.isEmpty() && hasDepartures) {
-            // println("\tbut noVM")
-            if (ticked()) {
-                //  println("\t\tbut ticked")
-                refreshAdapterFromStop()
-            }
-            return
-        }
-        if (timetableType == "departure" && stopSegment!!.contains(plateId)) {
-            // println("\tthere’s still vm")
-            if (vmDepartures != null)
-                this.vmDepartures[plateId] = vmDepartures
-            else
-                this.vmDepartures.remove(plateId)
-            refreshAdapterFromStop()
+    override fun onDeparturesReady(departures: List<Departure>, plateId: Plate.ID?) {
+        if (plateId == null) {
+            this.departures.clear()
+            this.departures[Plate.ID.dummy] = departures
+        } else {
+            this.departures.remove(Plate.ID.dummy)
+            this.departures[plateId] = departures
         }
+        if (timetableType == TIMETABLE_TYPE_FULL)
+            return
+        refreshAdapter()
     }
 
-    private fun ticked() = Calendar.getInstance().timeInMillis - lastUpdated >= VmClient.TICK_6_ZINA_TIM_WITH_MARGIN
+    private fun refreshAdapter() {
+        if (timetableType == TIMETABLE_TYPE_FULL) {
+            @Suppress("UNCHECKED_CAST")
+            adapter.departures = fullDepartures[(dateSpinner.selectedItem as ServiceAdapter.RowItem).service]
+        } else {
+            val now = Calendar.getInstance()
+            val seconds = now.secondsAfterMidnight()
+            adapter.departures = this.departures.flatMap { it.value }.sortedBy { it.timeTill(seconds) }
+        }
+        adapter.notifyDataSetChanged()
+    }
 
     override fun onTimetableDownload(result: String?) {
         val message: String = when (result) {
@@ -203,17 +173,12 @@         try {
             Snackbar.make(findViewById(R.id.stop_layout), message, Snackbar.LENGTH_LONG).show()
         } catch (e: IllegalArgumentException) {
         }
-        timetable = Timetable.getTimetable(this, true)
-        if (sourceType == SOURCE_TYPE_STOP)
-            refreshAdapterFromStop()
-    }
-
-    private fun selectTodayPage() {
-        tabs.getTabAt(sectionsPagerAdapter!!.todayTab())!!.select()
+        providerProxy.refreshTimetable(this)
     }
 
     override fun onCreateOptionsMenu(menu: Menu): Boolean {
-        menuInflater.inflate(R.menu.menu_stop, menu)
+        if (providerProxy.mode == ProviderProxy.MODE_FULL)
+            menuInflater.inflate(R.menu.menu_stop, menu)
         return true
     }
 
@@ -221,22 +186,42 @@     override fun onOptionsItemSelected(item: MenuItem): Boolean {
         val id = item.itemId
 
         if (id == R.id.action_change_type) {
-            if (timetableType == "departure") {
-                timetableType = "full"
+            if (timetableType == TIMETABLE_TYPE_DEPARTURE) {
+                timetableType = TIMETABLE_TYPE_FULL
+
                 item.icon = (ResourcesCompat.getDrawable(resources, R.drawable.ic_timetable_departure, this.theme))
-                sectionsPagerAdapter?.relativeTime = false
-                if (sourceType == SOURCE_TYPE_STOP)
-                    refreshAdapter(timetable.getStopDepartures(stopSegment!!.stop))
-                else
-                    refreshAdapter(favourite!!.fullTimetable())
+                adapter.relativeTime = false
+                if (fullDepartures.isEmpty())
+                    if (sourceType == SOURCE_TYPE_STOP)
+                        fullDepartures.putAll(providerProxy.getFullTimetable(stopCode))
+                    else
+                        fullDepartures.putAll(favourite!!.fullTimetable())
+
+                dateSpinner.let { spinner ->
+                    spinner.adapter = ServiceAdapter(this, R.layout.toolbar_spinner_item, fullDepartures.keys.map {
+                        ServiceAdapter.RowItem(it, providerProxy.describeService(it, this)!!)
+                    }.sorted()).apply {
+                        setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+                    }
+                    spinner.visibility = View.VISIBLE
+                    spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+                        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+                            refreshAdapter()
+                        }
+
+                        override fun onNothingSelected(parent: AdapterView<*>?) {
+                        }
+
+                    }
+                }
+
+                refreshAdapter()
             } else {
-                timetableType = "departure"
+                dateSpinner.visibility = View.GONE
+                timetableType = TIMETABLE_TYPE_DEPARTURE
                 item.icon = (ResourcesCompat.getDrawable(resources, R.drawable.ic_timetable_full, this.theme))
-                sectionsPagerAdapter?.relativeTime = true
-                if (sourceType == SOURCE_TYPE_STOP)
-                    refreshAdapterFromStop()
-                else
-                    refreshAdapter(favourite!!.allDepartures())
+                adapter.relativeTime = true
+                refreshAdapter()
             }
             return true
         }
@@ -247,104 +232,10 @@
     override fun onDestroy() {
         super.onDestroy()
         receiver.removeOnTimetableDownloadListener(context)
-        if (sourceType == SOURCE_TYPE_STOP) {
-            receiver.removeOnVmListener(context)
-            val intent = Intent(this, VmClient::class.java)
-            intent.putExtra("stop", stopSegment)
-            intent.action = "remove"
-            startService(intent)
-        } else
-            favourite!!.deregisterOnVm(receiver, context)
+        if (sourceType == SOURCE_TYPE_STOP)
+            providerProxy.unsubscribeFromDepartures(subscriptionId, this)
+        else
+            favourite!!.unsubscribeFromDepartures(this)
         unregisterReceiver(receiver)
-    }
-
-    inner class SectionsPagerAdapter(fm: FragmentManager, var departures: Map<AgencyAndId, List<Departure>>?) : FragmentStatePagerAdapter(fm) {
-        var relativeTime = true
-
-        override fun getItem(position: Int): Fragment {
-            if (departures == null)
-                return PlaceholderFragment.newInstance(null, relativeTime) { updateFabVisibility(it) }
-            if (departures!!.isEmpty())
-                return PlaceholderFragment.newInstance(ArrayList(), relativeTime) { updateFabVisibility(it) }
-            val sat = try {
-                timetable.getServiceFor(Calendar.SATURDAY)
-            } catch (e: IllegalArgumentException) {
-                null
-            }
-            val sun = try {
-                timetable.getServiceFor(Calendar.SUNDAY)
-            } catch (e: IllegalArgumentException) {
-                null
-            }
-            val list: List<Departure> = when (position) {
-                1 -> departures!![sat] ?: ArrayList()
-                2 -> departures!![sun] ?: ArrayList()
-                0 -> try {
-                    departures!!
-                            .filter { it.key != sat && it.key != sun }
-                            .toList()[0].second
-                } catch (e: IndexOutOfBoundsException) {
-                    ArrayList<Departure>()
-                }
-                else -> throw IndexOutOfBoundsException("No tab at index $position")
-            }
-            return PlaceholderFragment.newInstance(list, relativeTime) { updateFabVisibility(it) }
-        }
-
-        override fun getCount() = 3
-
-        override fun getItemPosition(obj: Any): Int {
-            return PagerAdapter.POSITION_NONE
-        }
-
-        fun todayTab(): Int {
-            return Calendar.getInstance().getMode()
-        }
-    }
-
-    class PlaceholderFragment: Fragment() {
-        lateinit var updater: (Int) -> Unit
-        override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
-            val rootView = inflater.inflate(R.layout.fragment_stop, container, false)
-
-            val layoutManager = LinearLayoutManager(activity)
-            val departuresList: RecyclerView = rootView.findViewById(R.id.departuresList)
-            val departures = arguments?.getStringArrayList("departures")?.map { Departure.fromString(it) }
-            if (departures != null && departures.isNotEmpty())
-                departuresList.addItemDecoration(DividerItemDecoration(departuresList.context, layoutManager.orientation))
-
-
-            departuresList.adapter = DeparturesAdapter(activity as Context, departures,
-                    arguments?.get("relativeTime") as Boolean)
-            departuresList.layoutManager = layoutManager
-
-            departuresList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
-                override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {}
-                override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
-                    updater(dy)
-                    super.onScrolled(recyclerView, dx, dy)
-                }
-            })
-            return rootView
-        }
-
-        companion object {
-            fun newInstance(departures: List<Departure>?, relativeTime: Boolean, updater: (Int) -> Unit): PlaceholderFragment {
-                val fragment = PlaceholderFragment()
-                fragment.updater = updater
-                val args = Bundle()
-                if (departures != null) {
-                    if (departures.isNotEmpty()) {
-                        val d = ArrayList<String>()
-                        departures.mapTo(d) { it.toString() }
-                        args.putStringArrayList("departures", d)
-                    } else
-                        args.putStringArrayList("departures", ArrayList<String>())
-                }
-                args.putBoolean("relativeTime", relativeTime)
-                fragment.arguments = args
-                return fragment
-            }
-        }
     }
 }




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt
index eacf1d6d40244cf2c40982f9514211603d2c683f..a81d3536efc67b4b34a1b4b77337be6ba2379288 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt
@@ -7,18 +7,16 @@ import android.view.View
 import android.view.ViewGroup
 import kotlinx.android.synthetic.main.activity_stop_specify.*
 import ml.adamsprogs.bimba.R
-import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
-import ml.adamsprogs.bimba.models.Timetable
 import android.content.Context
 import android.widget.TextView
 import android.support.v7.widget.LinearLayoutManager
 import android.support.v7.widget.RecyclerView
 import android.view.LayoutInflater
+import ml.adamsprogs.bimba.ProviderProxy
 
 class StopSpecifyActivity : AppCompatActivity() {
 
     companion object {
-        const val EXTRA_STOP_IDS = "stopIds"
         const val EXTRA_STOP_NAME = "stopName"
     }
 
@@ -26,22 +24,24 @@     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_stop_specify)
 
-        val ids = intent.getStringExtra(EXTRA_STOP_IDS).split(",").map { AgencyAndId(it) }.toSet()
         val name = intent.getStringExtra(EXTRA_STOP_NAME)
-        val timetable = Timetable.getTimetable(this)
-        val headlines = timetable.getHeadlinesForStop(ids)
+        val providerProxy = ProviderProxy(this)
+        providerProxy.getSheds(name) {
+            val layoutManager = LinearLayoutManager(this)
+            val departuresList: RecyclerView = list_view
 
-        val layoutManager = LinearLayoutManager(this)
-        val departuresList: RecyclerView = list_view
+            departuresList.adapter = ShedAdapter(this, it, name)
+            departuresList.layoutManager = layoutManager
+        }
+        /*val timetable = Timetable.getTimetable(this)
+        val headlines = timetable.getHeadlinesForStop(name)*/
 
-        departuresList.adapter = ShedAdapter(this, headlines)
-        departuresList.layoutManager = layoutManager
 
         setSupportActionBar(toolbar)
         supportActionBar?.title = name
     }
 
-    class ShedAdapter(val context: Context, private val values: Map<AgencyAndId, Pair<String, Set<String>>>) :
+    class ShedAdapter(val context: Context, private val values: Map<String, Set<String>>, private val stopName: String) :
             RecyclerView.Adapter<ShedAdapter.ViewHolder>() {
         override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
             val context = parent.context
@@ -55,15 +55,16 @@         override fun getItemCount(): Int = values.size
 
         override fun onBindViewHolder(holder: ViewHolder, position: Int) {
             holder.root.setOnClickListener {
-                val id = values.entries.sortedBy { it.value.first }[position].key
+                val code = values.keys.sorted()[position]
                 val intent = Intent(context, StopActivity::class.java)
                 intent.putExtra(StopActivity.SOURCE_TYPE, StopActivity.SOURCE_TYPE_STOP)
-                intent.putExtra(StopActivity.EXTRA_STOP_ID, id)
+                intent.putExtra(StopActivity.EXTRA_STOP_CODE, code)
+                intent.putExtra(StopActivity.EXTRA_STOP_NAME, stopName)
                 context.startActivity(intent)
             }
-            holder.stopCode.text = values.values.sortedBy { it.first }[position].first
-            holder.stopHeadlines.text = values.values.sortedBy { it.first }[position].second
-                    .sortedBy { it } // fixme<p:1> natural sort
+            holder.stopCode.text = values.keys.sorted()[position]
+            holder.stopHeadlines.text = values.entries.sortedBy { it.key }[position].value
+                    .sortedBy { it.split(" → ")[0].toInt() }
                     .joinToString()
         }
 




diff --git a/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt b/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt
index 2c8e92efd8f69559ae0ec5cc2eadda253d49c80f..d937e039aa9cc926d197f7bebe913883845281a5 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt
@@ -4,7 +4,6 @@ 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
 
 
@@ -34,12 +33,19 @@         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)
+                val stopSegment = StopSegment(it.asJsonObject["stop"].asString, null)
+                val plates = it.asJsonObject["plates"].let { jsonPlates ->
+                    if (jsonPlates == null || jsonPlates.isJsonNull)
+                        null
+                    else {
+                        HashSet<Plate.ID>().apply {
+                            jsonPlates.asJsonArray.map {
+                                Plate.ID(it.asJsonObject["line"].asString,
+                                        it.asJsonObject["stop"].asString,
+                                        it.asJsonObject["headsign"].asString)
+                            }
+                        }
+                    }
                 }
                 stopSegment.plates = plates
                 stopSegment
@@ -79,9 +85,11 @@         positionIndex.remove(name)
         serialize()
     }
 
-    fun delete(name: String, plate: Plate.ID) {
-        favourites[name]?.delete(plate)
-        serialize()
+    fun delete(name: String, plate: Plate.ID): Boolean {
+        return favourites[name]?.delete(plate).let {
+            serialize()
+            it
+        } ?: false
     }
 
     private fun serialize() {
@@ -90,15 +98,20 @@         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.addProperty("stop", timetable.stop)
+                val plates =
+                        if (timetable.plates == null)
+                            JsonNull.INSTANCE
+                        else
+                            JsonArray().apply {
+                                for (plate in timetable.plates ?: HashSet()) {
+                                    val element = JsonObject()
+                                    element.addProperty("stop", plate.stop)
+                                    element.addProperty("line", plate.line)
+                                    element.addProperty("headsign", plate.headsign)
+                                    add(element)
+                                }
+                            }
                 segment.add("plates", plates)
                 timetables.add(segment)
             }
@@ -115,9 +128,9 @@     fun merge(names: List, context: Context) {
         if (names.size < 2)
             return
 
-        val newCache = HashMap<AgencyAndId, ArrayList<Departure>>()
+        val newCache = HashMap<String, ArrayList<Departure>>()
         names.forEach {
-            favourites[it]!!.fullDepartures.forEach {
+            favourites[it]!!.fullTimetable().forEach {
                 if (newCache[it.key] == null)
                     newCache[it.key] = ArrayList()
                 newCache[it.key]!!.addAll(it.value)
@@ -147,18 +160,6 @@         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? {




diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/CacheManager.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/CacheManager.kt
deleted file mode 100644
index 71130764f796ebe17e68897bd6ec9899accfe5b1..0000000000000000000000000000000000000000
--- a/app/src/main/java/ml/adamsprogs/bimba/datasources/CacheManager.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-package ml.adamsprogs.bimba.datasources
-
-import android.content.Context
-import android.content.SharedPreferences
-import ml.adamsprogs.bimba.models.Plate
-import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
-
-class CacheManager private constructor(context: Context) {
-    companion object {
-        private var manager: CacheManager? = null
-        fun getCacheManager(context: Context): CacheManager {
-            return if (manager == null) {
-                manager = CacheManager(context)
-                manager!!
-            } else
-                manager!!
-        }
-
-        val MAX_SIZE = 40
-    }
-
-    private var cachePreferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.cachePreferences.cache", Context.MODE_PRIVATE)
-    private var cacheHitsPreferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.cachePreferences.cacheHits", Context.MODE_PRIVATE)
-
-    private var cache: HashMap<String, Plate> = HashMap()
-    private var cacheHits: HashMap<String, Int> = HashMap()
-
-    fun keys(): List<Plate> {
-        return cache.map {
-            Plate(Plate.ID(
-                    AgencyAndId.convertFromString(it.key.split("@")[0]),
-                    AgencyAndId.convertFromString(it.key.split("@")[1].split(">")[0]),
-                    it.key.split(">")[1]),
-                    null)
-        }
-    }
-
-    fun hasAll(plates: HashSet<Plate>): Boolean {
-        plates
-                .filterNot { has(it) }
-                .forEach { return false }
-        return true
-    }
-
-    fun hasAny(plates: HashSet<Plate>): Boolean {
-        plates
-                .filter { has(it) }
-                .forEach { return true }
-        return false
-    }
-
-    fun has(plate: Plate): Boolean {
-        return cache.containsKey(key(plate))
-    }
-
-    fun push(plates: HashSet<Plate>) {
-        val removeNumber = cache.size + plates.size - MAX_SIZE
-        val editor = cachePreferences.edit()
-        val editorCacheHits = cacheHitsPreferences.edit()
-        cacheHits.map { "${it.value}|${it.key}" }.sortedBy { it }.slice(0 until removeNumber).forEach {
-            val key = it.split("|")[1]
-            cache.remove(key)
-            editor.remove(key)
-        }
-        for (plate in plates) {
-            val key = key(plate)
-            cache[key] = plate
-            cacheHits[key] = 0
-            editor.putString(key, cache[key].toString())
-            editorCacheHits.putInt(key, 0)
-        }
-        editor.apply()
-        editorCacheHits.apply()
-    }
-
-    fun push(plate: Plate) {
-        val editorCache = cachePreferences.edit()
-        val editorCacheHits = cacheHitsPreferences.edit()
-        if (cacheHits.size == MAX_SIZE) {
-            val key = cacheHits.minBy { it.value }?.key
-            cache.remove(key)
-            editorCache.remove(key)
-            cacheHits.remove(key)
-            editorCacheHits.remove(key)
-        }
-        val key = key(plate)
-        cache[key] = plate
-        cacheHits[key] = 0
-        editorCache.putString(key, plate.toString())
-        editorCacheHits.putInt(key, 0)
-        editorCache.apply()
-        editorCacheHits.apply()
-    }
-
-    fun get(plates: HashSet<Plate>): HashSet<Plate> {
-        val result = HashSet<Plate>()
-        for (plate in plates) {
-            val value = get(plate)
-            if (value == null)
-                result.add(plate)
-            else
-                result.add(value)
-        }
-        return result
-    }
-
-    fun get(plate: Plate): Plate? {
-        if (!has(plate))
-            return null
-        val key = key(plate)
-        val hits = cacheHits[key]
-        if (hits != null)
-            cacheHits[key] = hits + 1
-        return cache[key]
-    }
-
-    fun recreate(stopDeparturesByPlates: Set<Plate>) {
-        stopDeparturesByPlates.forEach { cache[key(it)] = it }
-    }
-
-    init {
-        cache = cacheFromString(cachePreferences.all)
-        @Suppress("UNCHECKED_CAST")
-        cacheHits = cacheHitsPreferences.all as HashMap<String, Int>
-    }
-
-    private fun cacheFromString(preferences: Map<String, *>): HashMap<String, Plate> {
-        val result = HashMap<String, Plate>()
-        for ((key, value) in preferences.entries) {
-            result[key] = Plate.fromString(value as String)
-        }
-        return result
-    }
-
-    private fun key(plate: Plate) = "${plate.id.line}@${plate.id.stop}>${plate.id.headsign}"
-}
\ 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 7fcb3e1ac4c36f0e0bcafc7fcc995bb468e920c7..8c39cb5c95f88327c5aa654d3d4b84e701fdcad6 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt
@@ -31,7 +31,7 @@
         if (intent != null) {
             notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
             val prefs = this.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE)!!
-            if (!NetworkStateReceiver.isNetworkAvailable(this)) {
+            if (!NetworkStateReceiver.isNetworkAvailable()) {
                 sendResult(RESULT_NO_CONNECTIVITY)
                 return
             }
@@ -57,7 +57,6 @@                     sendResult(RESULT_UP_TO_DATE)
                     return
                 }
                 if (httpCon.responseCode != HttpsURLConnection.HTTP_OK) {
-                    println(httpCon.responseMessage)
                     sendResult(RESULT_NO_CONNECTIVITY)
                     return
                 }
@@ -94,7 +93,7 @@             prefsEditor.putString("etag", newETag)
             prefsEditor.apply()
 
             val oldDb = File(getSecondaryExternalFilesDir(), "timetable.db")
-            gtfsDb.renameTo(oldDb) // todo<p:1> delete old before downloading (may require stopping VmClient), and mutex with VmClient
+            gtfsDb.renameTo(oldDb) // todo<p:1> delete old before downloading (may require stopping VmService), and mutex with VmService
 
             cancelNotification()
 




diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt
index bf703cca766ddb831b2069b1e778f3a979858694..af652ab3dad6fec82a7bb93d85733aac2cc10f56 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt
@@ -1,256 +1,133 @@
 package ml.adamsprogs.bimba.datasources
 
-import android.app.Service
-import android.content.Intent
-import android.os.Handler
-import android.os.HandlerThread
-import android.os.IBinder
-import android.os.Process.THREAD_PRIORITY_BACKGROUND
-import com.google.gson.Gson
+import com.google.gson.*
+import kotlinx.coroutines.experimental.*
 import ml.adamsprogs.bimba.NetworkStateReceiver
-import ml.adamsprogs.bimba.calendarFromIso
-import ml.adamsprogs.bimba.models.*
-import okhttp3.FormBody
-import okhttp3.OkHttpClient
-import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
-import ml.adamsprogs.bimba.secondsAfterMidnight
+import ml.adamsprogs.bimba.models.Plate
+import ml.adamsprogs.bimba.models.StopSegment
+import ml.adamsprogs.bimba.models.suggestions.*
+import okhttp3.*
 import java.io.IOException
 import java.util.*
 import kotlin.collections.HashMap
 import kotlin.collections.HashSet
-import kotlin.concurrent.thread
 
-class VmClient : Service() {
+class VmClient {
     companion object {
-        const val ACTION_READY = "ml.adamsprogs.bimba.action.vm.ready"
-        const val EXTRA_DEPARTURES = "ml.adamsprogs.bimba.extra.vm.departures"
-        const val EXTRA_PLATE_ID = "ml.adamsprogs.bimba.extra.vm.plate"
-        const val TICK_6_ZINA_TIM = 12500L
-        const val TICK_6_ZINA_TIM_WITH_MARGIN = TICK_6_ZINA_TIM * 3 / 4
-    }
+        private var vmClient: VmClient? = null
 
-    private var handler: Handler? = null
-    private val tick6ZinaTim: Runnable = object : Runnable {
-        override fun run() {
-            handler!!.postDelayed(this, TICK_6_ZINA_TIM)
-            try {
-                for (plateId in requests.keys)
-                    downloadVM()
-            } catch (e: IllegalArgumentException) {
-            }
+        fun getVmClient(): VmClient {
+            if (vmClient == null)
+                vmClient = VmClient()
+            return vmClient!!
         }
     }
-    private val requests = HashMap<AgencyAndId, Set<Request>>()
-    private val vms = HashMap<AgencyAndId, Set<Plate>>()
-    private val timetable = try {
-        Timetable.getTimetable(this)
-    } catch (e: NullPointerException) {
-        null
-    }
 
-
-    override fun onCreate() {
-        val thread = HandlerThread("ServiceStartArguments", THREAD_PRIORITY_BACKGROUND)
-        thread.start()
-        handler = Handler(thread.looper)
-        handler!!.postDelayed(tick6ZinaTim, TICK_6_ZINA_TIM)
-    }
-
-    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
-        if (timetable == null)
-            return START_NOT_STICKY
-        val stopSegment = intent?.getParcelableExtra<StopSegment>("stop")!!
-        if (stopSegment.plates == null)
-            throw EmptyStopSegmentException()
-        val action = intent.action
-        val once = intent.getBooleanExtra("once", false)
-        if (action == "request") {
-            if (isAlreadyRequested(stopSegment)) {
-                incrementRequest(stopSegment)
-                sendResult(stopSegment)
-            } else {
-                if (!once)
-                    addRequest(stopSegment)
-                thread {
-                    downloadVM(stopSegment)
-                }
-            }
-        } else if (action == "remove") {
-            decrementRequest(stopSegment)
-            cleanRequests()
+    suspend fun getSheds(name: String): Map<String, Set<String>> {
+        val response = makeRequest("getBollardsByStopPoint", """{"name": "$name"}""")
+        if (!response.has("success"))
+            return emptyMap()
+        val rootObject = response["success"].asJsonObject["bollards"].asJsonArray
+        val result = HashMap<String, Set<String>>()
+        rootObject.forEach {
+            val code = it.asJsonObject["bollard"].asJsonObject["tag"].asString
+            result[code] = it.asJsonObject["directions"].asJsonArray.map {
+                """${it.asJsonObject["lineName"].asString} → ${it.asJsonObject["direction"].asString}"""
+            }.toSet()
         }
-        return START_STICKY
+        return result
     }
 
-    private fun cleanRequests() {
-        val newMap = HashMap<AgencyAndId, Set<Request>>()
-        requests.forEach {
-            newMap[it.key] = it.value.minus(it.value.filter { it.times == 0 })
-        }
-        newMap.forEach { requests[it.key] = it.value }
-    }
+    /*
+    suspend fun getPlatesByStopPoint(code: String): Set<Plate.ID>? {
+        val getTimesResponse = makeRequest("getTimes", """{"symbol": "$code"}""")
+        val name = getTimesResponse["success"].asJsonObject["bollard"].asJsonObject["name"].asString
 
-    private fun addRequest(stopSegment: StopSegment) {
-        if (requests[stopSegment.stop] == null) {
-            requests[stopSegment.stop] = stopSegment.plates!!
-                    .map { Request(it, 1) }
-                    .toSet()
-        } else {
-            var req = requests[stopSegment.stop]!!
-            stopSegment.plates!!.forEach {
-                val plate = it
-                if (req.any { it.plate == plate }) {
-                    req.filter { it.plate == plate }[0].times++
-                } else {
-                    req = req.plus(Request(it, 1))
-                }
-                requests[stopSegment.stop] = req
+        val bollards = getBollardsByStopPoint(name)
+        return bollards.filter {
+            it.key == code
+        }.values.flatMap {
+            it.map {
+                val (line, headsign) = it.split(" → ")
+                Plate.ID(AgencyAndId(line), AgencyAndId(code), headsign)
             }
+        }.toSet()
+    }*/
+
+    suspend fun getStops(pattern: String): List<StopSuggestion> {
+        val response = withContext(DefaultDispatcher) {
+            makeRequest("getStopPoints", """{"pattern": "$pattern"}""")
         }
-    }
 
-    private fun sendResult(stop: StopSegment) {
-        vms[stop.stop]?.filter {
-            val plate = it
-            stop.plates!!.any { it == plate.id }
-        }?.forEach { sendResult(it.id, it.departures?.get(today())) }
-    }
+        if (!response.has("success"))
+            return emptyList()
 
-    private fun today(): AgencyAndId {
-        return timetable!!.getServiceForToday()
-    }
+        val points = response["success"].asJsonArray.map { it.asJsonObject }
 
-    private fun incrementRequest(stopSegment: StopSegment) {
-        stopSegment.plates!!.forEach {
-            val plateId = it
-            requests[it.stop]!!.filter { it.plate == plateId }.forEach { it.times++ }
-        }
-    }
+        val names = HashSet<String>()
 
-    private fun decrementRequest(stopSegment: StopSegment) {
-        stopSegment.plates!!.forEach {
-            val plateId = it
-            requests[it.stop]!!.filter { it.plate == plateId }.forEach { it.times-- }
+        points.forEach {
+            val name = it["name"].asString
+            names.add(name)
         }
-    }
 
-    private fun isAlreadyRequested(stopSegment: StopSegment): Boolean {
-        val platesIn = requests[stopSegment.stop]?.map { it.plate }?.toSet()
-        val platesOut = stopSegment.plates
-        if (platesIn == null || platesIn.isEmpty())
-            return false
-        return (platesOut == platesIn || platesIn.containsAll(platesOut!!))
-    }
-
-
-    override fun onBind(intent: Intent): IBinder? {
-        return null
-    }
-
-    override fun onDestroy() {
+        return names.map { StopSuggestion(it, "", "") }
     }
 
-    @Synchronized
-    private fun downloadVM() {
-        vms.forEach {
-            downloadVM(StopSegment(it.key, it.value.map { it.id }.toSet()))
-        }
-    }
+    suspend fun makeRequest(method: String, data: String): JsonObject {
+        if (!NetworkStateReceiver.isNetworkAvailable())
+            return JsonObject()
 
-    private fun downloadVM(stopSegment: StopSegment) {
-        if (!NetworkStateReceiver.isNetworkAvailable(this)) {
-            vms[stopSegment.stop] = HashSet(stopSegment.plates!!.map { Plate(it, null) }.toSet())
-            stopSegment.plates!!.forEach {
-                sendResult(it, null)
-            }
-            return
-        }
-
-        val stopSymbol = timetable!!.getStopCode(stopSegment.stop)
         val client = OkHttpClient()
         val url = "http://www.peka.poznan.pl/vm/method.vm?ts=${Calendar.getInstance().timeInMillis}"
-        val formBody = FormBody.Builder()
-                .add("method", "getTimes")
-                .add("p0", "{\"symbol\": \"$stopSymbol\"}")
-                .build()
+        val body = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded; charset=UTF-8"),
+                "method=$method&p0=$data")
         val request = okhttp3.Request.Builder()
                 .url(url)
-                .post(formBody)
+                .post(body)
                 .build()
 
+
         val responseBody: String?
         try {
-            responseBody = client.newCall(request).execute().body()?.string()
+            responseBody = withContext(CommonPool) {
+                client.newCall(request).execute().body()?.string()
+            }
         } catch (e: IOException) {
-            stopSegment.plates!!.forEach {
-                sendResult(it, null)
-            }
-            return
+            return JsonObject()
         }
 
-        if (responseBody?.get(0) == '<') {
-            stopSegment.plates!!.forEach {
-                sendResult(it, null)
-            }
-            return
+        return try {
+            Gson().fromJson(responseBody, JsonObject::class.java)
+        } catch (e: JsonSyntaxException) {
+            JsonObject()
         }
-
-        val javaRootMapObject = Gson().fromJson(responseBody, HashMap::class.java)
-        val times = (javaRootMapObject["success"] as Map<*, *>)["times"] as List<*>
-        stopSegment.plates!!.forEach { downloadVM(it, times) }
-
     }
 
-    private fun downloadVM(plateId: Plate.ID, times: List<*>) {
-        val date = Calendar.getInstance()
-        val todayDay = "${date.get(Calendar.DATE)}".padStart(2, '0')
-        val todayMode = timetable!!.calendarToMode(AgencyAndId(timetable.getServiceForToday().id)) // fixme when no timetable use service == -1 for `today`
-
-        val departures = HashSet<Departure>()
-
-        times.forEach {
-            val thisLine = AgencyAndId((it as Map<*, *>)["line"] as String)
-            val thisHeadsign = it["direction"] as String
-            val thisPlateId = Plate.ID(thisLine, plateId.stop, thisHeadsign)
-            if (plateId == thisPlateId) {
-                val departureDay = (it["departure"] as String).split("T")[0].split("-")[2]
-                val departureTime = calendarFromIso(it["departure"] as String).secondsAfterMidnight()
-                val departure = Departure(plateId.line, todayMode, departureTime, false,
-                        ArrayList(), it["direction"] as String, it["realTime"] as Boolean,
-                        departureDay != todayDay, it["onStopPoint"] as Boolean)
-                departures.add(departure)
-            }
+    suspend fun getName(symbol: String): String? {
+        val timesResponse = withContext(DefaultDispatcher) {
+            makeRequest("getTimes", """{"symbol": "$symbol"}""")
         }
+        if (!timesResponse.has("success"))
+            return null
 
-        val departuresForPlate = HashMap<AgencyAndId, HashSet<Departure>>()
-        departuresForPlate[timetable.getServiceForToday()] = departures
-        val vm = vms[plateId.stop] ?: HashSet()
-        try {
-            (vm as HashSet).remove(vm.filter { it.id == plateId }[0])
-        } catch (e: IndexOutOfBoundsException) {
-        }
-        (vm as HashSet).add(Plate(plateId, departuresForPlate))
-        vms[plateId.stop] = vm
-        if (departures.isEmpty())
-            sendResult(plateId, null)
-        else
-            sendResult(plateId, departures)
+        return timesResponse["success"].asJsonObject["bollard"].asJsonObject["name"].asString
     }
 
-    private fun sendResult(plateId: Plate.ID, departures: HashSet<Departure>?) {
-        val broadcastIntent = Intent()
-        broadcastIntent.action = ACTION_READY
-        broadcastIntent.addCategory(Intent.CATEGORY_DEFAULT)
-        if (departures != null)
-            broadcastIntent.putStringArrayListExtra(EXTRA_DEPARTURES, departures.map { it.toString() } as ArrayList)
-        broadcastIntent.putExtra(EXTRA_PLATE_ID, plateId)
-        sendBroadcast(broadcastIntent)
-    }
+    suspend fun getDirections(symbol: String): StopSegment? {
+        val name = getName(symbol)
+        val directionsResponse = makeRequest("getBollardsByStopPoint", """{"name": "$name"}""")
 
-    data class Request(val plate: Plate.ID, var times: Int)
+        if (!directionsResponse.has("success"))
+            return null
 
-    class EmptyStopSegmentException : Exception()
+        return StopSegment(symbol,
+                directionsResponse["success"].asJsonObject["bollards"].asJsonArray.filter {
+                    it.asJsonObject["bollard"].asJsonObject["tag"].asString == symbol
+                }[0].asJsonObject["directions"].asJsonArray.map {
+                    it.asJsonObject.let { direction ->
+                        Plate.ID(direction["lineName"].asString, symbol, direction["direction"].asString)
+                    }
+                }.toSet())
+    }
 }
-
-//note application stops the service on exit
-




diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmService.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..710aa49f6b3ad7a7f661864ea4231306aa403f20
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmService.kt
@@ -0,0 +1,189 @@
+package ml.adamsprogs.bimba.datasources
+
+import android.app.Service
+import android.content.Intent
+import android.os.*
+import android.os.Process.THREAD_PRIORITY_BACKGROUND
+import com.google.gson.JsonObject
+import kotlinx.coroutines.experimental.android.UI
+import kotlinx.coroutines.experimental.*
+import ml.adamsprogs.bimba.NetworkStateReceiver
+import ml.adamsprogs.bimba.calendarFromIso
+import ml.adamsprogs.bimba.models.*
+import ml.adamsprogs.bimba.secondsAfterMidnight
+import java.util.*
+import kotlin.collections.*
+
+class VmService : Service() {
+    companion object {
+        const val ACTION_READY = "ml.adamsprogs.bimba.action.vm.ready"
+        const val EXTRA_DEPARTURES = "ml.adamsprogs.bimba.extra.vm.departures"
+        const val EXTRA_PLATE_ID = "ml.adamsprogs.bimba.extra.vm.plate"
+        const val EXTRA_STOP_CODE = "ml.adamsprogs.bimba.extra.vm.stop"
+        const val TICK_6_ZINA_TIM = 12500L
+        const val TICK_6_ZINA_TIM_WITH_MARGIN = TICK_6_ZINA_TIM * 3 / 4
+    }
+
+    private var handler: Handler? = null
+    private val tick6ZinaTim: Runnable = object : Runnable {
+        override fun run() {
+            handler!!.postDelayed(this, TICK_6_ZINA_TIM)
+            try {
+                for (plateId in requests.keys)
+                    launch(UI) {
+                        withContext(DefaultDispatcher) {
+                            downloadVM()
+                        }
+                    }
+            } catch (e: IllegalArgumentException) {
+            }
+        }
+    }
+    private val requests = HashMap<String, Int>()
+    private val vms = HashMap<String, Set<Plate>>()
+
+    override fun onCreate() {
+        val thread = HandlerThread("ServiceStartArguments", THREAD_PRIORITY_BACKGROUND)
+        thread.start()
+        handler = Handler(thread.looper)
+        handler!!.postDelayed(tick6ZinaTim, TICK_6_ZINA_TIM)
+    }
+
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        if (intent == null)
+            return START_STICKY
+        val stopCode = intent.getStringExtra("stop")!!
+        val action = intent.action
+        val once = intent.getBooleanExtra("once", false)
+        if (action == "request") {
+            if (isAlreadyRequested(stopCode)) {
+                incrementRequest(stopCode)
+                sendResult(stopCode)
+            } else {
+                if (!once)
+                    addRequest(stopCode)
+                launch(UI) {
+                    withContext(DefaultDispatcher) {
+                        downloadVM(stopCode)
+                    }
+                }
+            }
+        } else if (action == "remove") {
+            decrementRequest(stopCode)
+            cleanRequests()
+        }
+        return START_STICKY
+    }
+
+    private fun cleanRequests() {
+        requests.forEach {
+            if (it.value <= 0)
+                requests.remove(it.key)
+        }
+    }
+
+    private fun addRequest(stopCode: String) {
+        if (requests[stopCode] == null)
+            requests[stopCode] = 0
+        requests[stopCode] = requests[stopCode]!! + 1
+    }
+
+    private fun incrementRequest(stopCode: String) {
+        requests[stopCode] = requests[stopCode]!! + 1
+    }
+
+    private fun decrementRequest(stopCode: String) {
+        requests[stopCode] = requests[stopCode]!! - 1
+    }
+
+    private fun isAlreadyRequested(stopCode: String): Boolean {
+        return stopCode in requests
+    }
+
+
+    override fun onBind(intent: Intent): IBinder? {
+        return null
+    }
+
+    override fun onDestroy() {
+    }
+
+    private suspend fun downloadVM() {
+        vms.forEach {
+            downloadVM(it.key)
+        }
+    }
+
+    private suspend fun downloadVM(stopCode: String) {
+        if (!NetworkStateReceiver.isNetworkAvailable()) {
+            vms[stopCode] = emptySet()
+            sendResult(stopCode, null, null)
+            return
+        }
+
+        val javaRootMapObject = VmClient.getVmClient().makeRequest("getTimes", """{"symbol": "$stopCode"}""")
+
+        if (!javaRootMapObject.has("success")) {
+            sendResult(stopCode, null, null)
+            return
+        }
+
+        val times = (javaRootMapObject["success"].asJsonObject)["times"].asJsonArray.map { it.asJsonObject }
+        parseTimes(stopCode, times)
+    }
+
+    private fun parseTimes(stopCode: String, times: List<JsonObject>) {
+        val date = Calendar.getInstance()
+        val todayDay = "${date.get(Calendar.DATE)}".padStart(2, '0')
+
+        val departures = HashMap<Plate.ID, HashSet<Departure>>()
+
+        times.forEach {
+            val thisLine = it["line"].asString
+            val thisHeadsign = it["direction"].asString
+            val thisPlateId = Plate.ID(thisLine, stopCode, thisHeadsign)
+            if (departures[thisPlateId] == null)
+                departures[thisPlateId] = HashSet()
+            val departureDay = (it["departure"].asString).split("T")[0].split("-")[2]
+            val departureTime = calendarFromIso(it["departure"].asString).secondsAfterMidnight()
+            val departure = Departure(thisLine, listOf(-1), departureTime, false,
+                    ArrayList(), it["direction"].asString, it["realTime"].asBoolean,
+                    departureDay != todayDay, it["onStopPoint"].asBoolean)
+            departures[thisPlateId]!!.add(departure)
+        }
+
+        departures.forEach {
+            val departuresForPlate = HashMap<Int, HashSet<Departure>>()
+            departuresForPlate[-1] = it.value
+            val vm = HashSet<Plate>()
+            vm.add(Plate(it.key, departuresForPlate))
+            vms[stopCode] = vm
+            if (departures.isEmpty())
+                sendResult(stopCode, it.key, null)
+            else
+                sendResult(stopCode, it.key, it.value)
+        }
+
+    }
+
+    private fun sendResult(stopCode: String) {
+        vms[stopCode]?.forEach {
+            sendResult(it.id.stop, it.id, it.departures?.get(-1))
+        }
+
+    }
+
+    private fun sendResult(stopCode: String, plateId: Plate.ID?, departures: HashSet<Departure>?) {
+        val broadcastIntent = Intent()
+        broadcastIntent.action = ACTION_READY
+        broadcastIntent.addCategory(Intent.CATEGORY_DEFAULT)
+        if (departures != null)
+            broadcastIntent.putStringArrayListExtra(EXTRA_DEPARTURES, departures.map { it.toString() } as ArrayList)
+        broadcastIntent.putExtra(EXTRA_PLATE_ID, plateId)
+        broadcastIntent.putExtra(EXTRA_STOP_CODE, stopCode)
+        sendBroadcast(broadcastIntent)
+    }
+}
+
+//note application stops the service on exit
+




diff --git a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
index cc04631c1708de25c1758299bc19ba0978599b2c..a349dc85e76661e7745d7245c11cf93a3d450e75 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
 import android.content.Context
 import android.graphics.drawable.Drawable
 import android.os.Build
+import android.text.format.DateFormat
 import ml.adamsprogs.bimba.activities.StopActivity
 import java.io.*
 import java.text.SimpleDateFormat
@@ -80,7 +81,9 @@         else -> StopActivity.MODE_WORKDAYS
     }
 }
 
-internal fun CharSequence.safeSplit(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List<String> {
+internal fun CharSequence.safeSplit(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List<String>? {
+    if (this == "null")
+        return null
     if (this == "")
         return ArrayList()
     return this.split(*delimiters, ignoreCase = ignoreCase, limit = limit)
@@ -104,3 +107,23 @@         bytes = read(buffer)
     }
     return bytesCopied
 }
+
+internal fun Calendar.toNiceString(context: Context, withTime: Boolean = false): String {
+    val dateFormat = DateFormat.getMediumDateFormat(context)
+    val timeFormat = DateFormat.getTimeFormat(context)
+    val now = Calendar.getInstance()
+    val date = if (get(Calendar.YEAR) == now.get(Calendar.YEAR)) {
+        when {
+            get(Calendar.DAY_OF_YEAR) == now.get(Calendar.DAY_OF_YEAR) -> timeFormat.format(time)
+            now.apply { add(Calendar.DATE, -1) }.get(Calendar.DAY_OF_YEAR) == get(Calendar.DAY_OF_YEAR) -> "Yesterday"
+            else -> DateFormat.format("d MMM" as CharSequence, this.time) as String
+        }
+    } else
+        dateFormat.format(this.time)
+
+    return if (withTime) {
+        val time = timeFormat.format(this.time)
+        "$date, $time"
+    } else
+        date
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt
index 9989a5a13126f8a19094985c47c289382071c70b..9661f529ba9bd5d83a822b47ec43e022c1daa537 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt
@@ -1,6 +1,5 @@
 package ml.adamsprogs.bimba.models
 
-import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
 import ml.adamsprogs.bimba.safeSplit
 import ml.adamsprogs.bimba.secondsAfterMidnight
 import java.io.Serializable
@@ -8,7 +7,7 @@ import java.util.*
 import kotlin.collections.ArrayList
 import kotlin.collections.HashMap
 
-data class Departure(val line: AgencyAndId, val mode: List<Int>, val time: Int, val lowFloor: Boolean, //time in seconds since midnight
+data class Departure(val line: String, val mode: List<Int>, val time: Int, val lowFloor: Boolean, //time in seconds since midnight
                      val modification: List<String>, val headsign: String, val vm: Boolean = false,
                      var tomorrow: Boolean = false, val onStop: Boolean = false) {
 
@@ -28,7 +27,7 @@
     companion object {
         private fun filterDepartures(departures: List<Departure>, relativeTo: Int = Calendar.getInstance().secondsAfterMidnight()): Array<Serializable> {
             val filtered = ArrayList<Departure>()
-            val lines = HashMap<AgencyAndId, Int>()
+            val lines = HashMap<String, Int>()
             val sortedDepartures = departures.sortedBy { it.timeTill(relativeTo) }
             for (departure in sortedDepartures) {
                 val timeTill = departure.timeTill(relativeTo)
@@ -42,47 +41,20 @@             }
             return arrayOf(filtered, lines.all { it.value >= 3 })
         }
 
-        fun createDepartures(stopId: AgencyAndId): Map<AgencyAndId, List<Departure>> {
+        /*fun createDepartures(stopCode: String): Map<String, List<Departure>> {
             val timetable = Timetable.getTimetable()
-            val departures = timetable.getStopDepartures(stopId)
+            val departures = timetable.getStopDepartures(stopCode)
 
             return rollDepartures(departures)
-        }
-
-        fun rollDepartures(departures: Map<AgencyAndId, List<Departure>>): Map<AgencyAndId, List<Departure>> { //todo<p:2> it'd be nice to roll from tomorrow's real mode (Fri->Sat, Sat->Sun, Sun->Mon)
-            val rolledDepartures = HashMap<AgencyAndId, List<Departure>>()
-            departures.keys.forEach {
-                val (filtered, isFull) = filterDepartures(departures[it]!!)
-                if (isFull as Boolean) {
-                    @Suppress("UNCHECKED_CAST")
-                    rolledDepartures[it] = filtered as List<Departure>
-                } else {
-                    val (filteredTomorrow, _) = filterDepartures(departures[it]!!, 0)
-                    val departuresTomorrow = ArrayList<Departure>()
-                    @Suppress("UNCHECKED_CAST")
-                    (filteredTomorrow as List<Departure>).forEach {
-                        val departure = it.copy()
-                        departure.tomorrow = true
-                        departuresTomorrow.add(departure)
-                    }
-                    val (result, _) =
-                            @Suppress("UNCHECKED_CAST")
-                            filterDepartures((filtered as List<Departure>) + departuresTomorrow)
-                    val now = Calendar.getInstance().secondsAfterMidnight()
-                    @Suppress("UNCHECKED_CAST")
-                    rolledDepartures[it] = (result as List<Departure>).sortedBy { it.timeTill(now) }
-                }
-            }
-            return rolledDepartures
-        }
+        }*/
 
         fun fromString(string: String): Departure {
             val array = string.split("|")
             if (array.size != 9)
                 throw IllegalArgumentException()
-            val modification = array[4].safeSplit(";")
-            return Departure(AgencyAndId.convertFromString(array[0]),
-                    array[1].safeSplit(";").map { Integer.parseInt(it) },
+            val modification = array[4].safeSplit(";")!!
+            return Departure(array[0],
+                    array[1].safeSplit(";")!!.map { Integer.parseInt(it) },
                     Integer.parseInt(array[2]), array[3] == "true",
                     modification, array[5], array[6] == "true",
                     array[7] == "true", array[8] == "true")
@@ -96,5 +68,5 @@             time += 24 * 60 * 60
         return (time - relativeTo) / 60
     }
 
-    val lineText: String = line.id
+    val lineText: String = line
 }
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt
index 469a7cdc28422e9fbc937c2e77c7fc4a59f2242b..8e2a99c5f289cabf1caacd451ff3bd91cb26d5d1 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt
@@ -3,42 +3,28 @@
 import android.content.*
 import android.os.*
 import ml.adamsprogs.bimba.*
-import ml.adamsprogs.bimba.datasources.VmClient
-import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
 import java.io.File
 import java.math.BigInteger
 import java.security.SecureRandom
-import java.util.Calendar
 import kotlin.collections.*
 
-class Favourite : Parcelable, MessageReceiver.OnVmListener {
-    private var isRegisteredOnVmListener: Boolean = false
+class Favourite : Parcelable, ProviderProxy.OnDeparturesReadyListener {
     private val cacheDir: File
+    private lateinit var listener: ProviderProxy.OnDeparturesReadyListener
     var name: String
         private set
     var segments: HashSet<StopSegment>
         private set
-    private var vmDepartures = HashMap<Plate.ID, List<Departure>>()
-    var fullDepartures: Map<AgencyAndId, List<Departure>> = HashMap()
-        private set
-    val timetable = Timetable.getTimetable()
+    private var fullDepartures: Map<String, List<Departure>> = HashMap()
+    private var cache: List<Departure> = ArrayList()
+    private var listenerId = ""
 
     val size
         get() = segments.sumBy {
             it.size
         }
-    val isBackedByVm
-        get() = vmDepartures.isNotEmpty()
 
-    private val onVmPreparedListeners = HashSet<OnVmPreparedListener>()
-
-    fun addOnVmPreparedListener(listener: OnVmPreparedListener) {
-        onVmPreparedListeners.add(listener)
-    }
-
-    fun removeOnVmPreparedListener(listener: OnVmPreparedListener) {
-        onVmPreparedListeners.remove(listener)
-    }
+    private val providerProxy: ProviderProxy
 
     constructor(parcel: Parcel) {
         this.name = parcel.readString()
@@ -54,26 +40,29 @@         val mapDir = File(parcel.readString())
 
         val mapString = mapDir.readText()
 
-        val map = HashMap<AgencyAndId, List<Departure>>()
-        mapString.safeSplit("%").forEach {
+        val map = HashMap<String, List<Departure>>()
+        mapString.safeSplit("%")!!.forEach { it ->
             val (k, v) = it.split("#")
-            map[AgencyAndId(k)] = v.split("&").map { Departure.fromString(it) }
+            map[k] = v.split("&").map { Departure.fromString(it) }
         }
         this.fullDepartures = map
         mapDir.delete()
+        providerProxy = ProviderProxy()
     }
 
-    constructor(name: String, segments: HashSet<StopSegment>, cache: Map<AgencyAndId, List<Departure>>, context: Context) {
+    constructor(name: String, segments: HashSet<StopSegment>, cache: Map<String, List<Departure>>, context: Context) {
         this.fullDepartures = cache
         this.name = name
         this.segments = segments
         this.cacheDir = context.cacheDir
+        providerProxy = ProviderProxy(context)
     }
 
     constructor(name: String, timetables: HashSet<StopSegment>, context: Context) {
         this.name = name
         this.segments = timetables
         this.cacheDir = context.cacheDir
+        providerProxy = ProviderProxy(context)
 
     }
 
@@ -94,7 +83,7 @@         dest?.writeString(mapFile.absolutePath)
 
         var isFirst = true
         var map = ""
-        fullDepartures.forEach {
+        fullDepartures.forEach { it ->
             if (isFirst)
                 isFirst = false
             else
@@ -105,49 +94,13 @@         }
         mapFile.writeText(map)
     }
 
-    private fun filterVmDepartures() {
-        val now = Calendar.getInstance().secondsAfterMidnight()
-        this.vmDepartures.forEach {
-            val newVms = it.value
-                    .filter { it.timeTill(now) >= 0 }.sortedBy { it.timeTill(now) }
-            this.vmDepartures[it.key] = newVms
-        }
-    }
-
-    fun delete(plateId: Plate.ID) {
+    fun delete(plateId: Plate.ID): Boolean {
         segments.forEach {
-            it.remove(plateId)
+            if (!it.remove(plateId))
+                return false
         }
         removeFromCache(plateId)
-    }
-
-    fun registerOnVm(receiver: MessageReceiver, context: Context) {
-        if (!isRegisteredOnVmListener) {
-            receiver.addOnVmListener(this)
-            isRegisteredOnVmListener = true
-
-
-            segments.forEach {
-                val intent = Intent(context, VmClient::class.java)
-                intent.putExtra("stop", it)
-                intent.action = "request"
-                context.startService(intent)
-            }
-        }
-    }
-
-    fun deregisterOnVm(receiver: MessageReceiver, context: Context) {
-        if (isRegisteredOnVmListener) {
-            receiver.removeOnVmListener(this)
-            isRegisteredOnVmListener = false
-
-            segments.forEach {
-                val intent = Intent(context, VmClient::class.java)
-                intent.putExtra("stop", it)
-                intent.action = "remove"
-                context.startService(intent)
-            }
-        }
+        return true
     }
 
     fun rename(newName: String) {
@@ -164,80 +117,40 @@             return arrayOfNulls(size)
         }
     }
 
-    fun nextDeparture(): Departure? {
-        val now = Calendar.getInstance().secondsAfterMidnight()
-        filterVmDepartures()
-        if (segments.isEmpty() && vmDepartures.isEmpty())
-            return null
-
-        if (vmDepartures.isNotEmpty()) {
-            return vmDepartures.flatMap { it.value }
-                    .minBy {
-                        it.timeTill(now)
-                    }
-        }
+    fun nextDeparture() =
+            if (cache.isEmpty())
+                null
+            else
+                cache.sortedBy { it.time }[0]
 
-        val full = fullTimetable()
 
-        val twoDayDepartures = try {
-            Departure.rollDepartures(full)[timetable.getServiceForToday()]
-        } catch (e: IllegalArgumentException) {
-            listOf<Departure>()
-        }
-
-        if (twoDayDepartures?.isEmpty() != false)
-            return null
-
-        return twoDayDepartures[0]
+    fun fullTimetable(): Map<String, List<Departure>> {
+        if (fullDepartures.isEmpty())
+            fullDepartures = providerProxy.getFullTimetable(segments)
+        return fullDepartures
     }
 
-    fun allDepartures(): Map<AgencyAndId, List<Departure>> {
-        if (vmDepartures.isNotEmpty()) {
-            val now = Calendar.getInstance().secondsAfterMidnight()
-            val departures = HashMap<AgencyAndId, List<Departure>>()
-            val today = timetable.getServiceForToday()
-            departures[today] = vmDepartures.flatMap { it.value }.sortedBy { it.timeTill(now) }
-            return departures
+    private fun removeFromCache(plate: Plate.ID) {
+        val map = HashMap<String, List<Departure>>()
+        fullDepartures
+        fullDepartures.forEach { it ->
+            map[it.key] = it.value.filter { plate.line != it.line || plate.headsign != it.headsign }
         }
-
-        val departures = fullTimetable()
-        return Departure.rollDepartures(departures)
+        fullDepartures = map
     }
 
-    fun fullTimetable() =
-            if (fullDepartures.isNotEmpty())
-                fullDepartures
-            else {
-                fullDepartures = timetable.getStopDeparturesBySegments(segments)
-                fullDepartures
-
-            }
-
-
-    override fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID) {
-        val now = Calendar.getInstance().secondsAfterMidnight()
-        if (segments.any { it.contains(plateId) }) {
-            if (vmDepartures == null)
-                this.vmDepartures.remove(plateId)
-            else
-                this.vmDepartures[plateId] = vmDepartures.sortedBy { it.timeTill(now) }
-        }
-        filterVmDepartures()
-        onVmPreparedListeners.forEach {
-            it.onVmPrepared()
-        }
+    fun subscribeForDepartures(listener: ProviderProxy.OnDeparturesReadyListener, context: Context): String {
+        this.listener = listener
+        listenerId = providerProxy.subscribeForDepartures(segments, this, context)
+        return listenerId
     }
 
-    private fun removeFromCache(plate: Plate.ID) {
-        val map = HashMap<AgencyAndId, List<Departure>>()
-        fullDepartures
-        fullDepartures.forEach {
-            map[it.key] = it.value.filter { plate.line != it.line || plate.headsign != it.headsign }
-        }
-        fullDepartures = map
+    override fun onDeparturesReady(departures: List<Departure>, plateId: Plate.ID?) {
+        cache = departures
+        listener.onDeparturesReady(departures, plateId)
     }
 
-    interface OnVmPreparedListener {
-        fun onVmPrepared()
+    fun unsubscribeFromDepartures(context: Context) {
+        providerProxy.unsubscribeFromDepartures(listenerId, context)
     }
 }




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt
index 43ff657d166e562c108df2eeacc2aa2031be0cd1..d4f7d8cc0dd3dc9e147c63f4182cbc1e009c64e7 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt
@@ -1,9 +1,8 @@
 package ml.adamsprogs.bimba.models
 
-import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
 import java.io.Serializable
 
-data class Plate(val id: ID, val departures: HashMap<AgencyAndId, HashSet<Departure>>?) {
+data class Plate(val id: ID, val departures: HashMap<Int, HashSet<Departure>>?) {
     override fun toString(): String {
         var result = "${id.line}=${id.stop}=${id.headsign}={"
         if (departures != null) {
@@ -19,29 +18,26 @@         return result
     }
 
     companion object {
-        fun fromString(string: String): Plate {
+        /*fun fromString(string: String): Plate {
             val (lineStr, stopStr, headsign, departuresString) = string.split("=")
-            val line = AgencyAndId.convertFromString(lineStr)
-            val stop = AgencyAndId.convertFromString(stopStr)
-            val departures = HashMap<AgencyAndId, HashSet<Departure>>()
+            val departures = HashMap<Int, HashSet<Departure>>()
             departuresString.replace("{", "").replace("}", "").split(";")
                     .filter { it != "" }
                     .forEach {
                         try {
                             val (serviceStr, depStr) = it.split(":")
                             val dep = Departure.fromString(depStr)
-                            val service = AgencyAndId.convertFromString(serviceStr)
-                            if (departures[service] == null)
-                                departures[service] = HashSet()
-                            departures[service]!!.add(dep)
+                            if (departures[serviceStr] == null)
+                                departures[serviceStr] = HashSet()
+                            departures[serviceStr]!!.add(dep)
                         } catch (e: IllegalArgumentException) {
                         }
                     }
-            return Plate(ID(line, stop, headsign), departures)
+            return Plate(ID(lineStr, stopStr, headsign), departures)
         }
 
-        fun join(set: Set<Plate>): HashMap<AgencyAndId, ArrayList<Departure>> {
-            val departures = HashMap<AgencyAndId, ArrayList<Departure>>()
+        fun join(set: Set<Plate>): HashMap<String, ArrayList<Departure>> {
+            val departures = HashMap<String, ArrayList<Departure>>()
             for (plate in set) {
                 for ((mode, d) in plate.departures!!) {
                     if (departures[mode] == null)
@@ -53,15 +49,17 @@             for ((mode, _) in departures) {
                 departures[mode]?.sortBy { it.time }
             }
             return departures
-        }
+        }*/
     }
 
-    data class ID(val line: AgencyAndId, val stop: AgencyAndId, val headsign: String) : Serializable {
+    data class ID(val line: String, val stop: String, val headsign: String) : Serializable {
         companion object {
+            val dummy = Plate.ID("", "", "")
+
             fun fromString(string: String): ID {
                 val (line, stop, headsign) = string.split("|")
-                return ID(AgencyAndId.convertFromString(line),
-                        AgencyAndId.convertFromString(stop), headsign)
+                return ID(line,
+                        stop, headsign)
             }
         }
 




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt b/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt
index 5197e6e17237a3b8ab3e28880b875321ddb6fda3..50f2fb4860b39f77025541cb341a46a18ab93e2c 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt
@@ -2,13 +2,12 @@ package ml.adamsprogs.bimba.models
 
 import android.os.Parcel
 import android.os.Parcelable
-import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
 import ml.adamsprogs.bimba.safeSplit
 
-data class StopSegment(val stop: AgencyAndId, var plates: Set<Plate.ID>?) : Parcelable {
+data class StopSegment(val stop: String, var plates: Set<Plate.ID>?) : Parcelable {
     constructor(parcel: Parcel) : this(
-            parcel.readSerializable() as AgencyAndId,
-            parcel.readString().safeSplit(";").map { Plate.ID.fromString(it) }.toSet()
+            parcel.readSerializable() as String,
+            parcel.readString().safeSplit(";")?.map { Plate.ID.fromString(it) }?.toSet()
     )
 
     companion object CREATOR : Parcelable.Creator<StopSegment> {
@@ -21,16 +20,12 @@             return arrayOfNulls(size)
         }
     }
 
-    fun fillPlates() {
-        plates = Timetable.getTimetable().getPlatesForStop(stop)
-    }
-
     override fun writeToParcel(dest: Parcel?, flags: Int) {
         dest?.writeSerializable(stop)
         if (plates != null)
             dest?.writeString(plates!!.joinToString(";") { it.toString() })
         else
-            dest?.writeString("")
+            dest?.writeString("null")
     }
 
     override fun describeContents(): Int {
@@ -56,17 +51,33 @@         return false
     }
 
     override fun hashCode(): Int {
-        return super.hashCode()
+        var hashCode = stop.hashCode()
+        plates?.forEach { hashCode = 31 * hashCode + it.hashCode() }
+        return hashCode
+    }
+
+    operator fun contains(plateId: Plate.ID): Boolean {
+        if (plates == null)
+            return plateId.stop == stop
+        return plates!!.contains(plateId)
     }
 
-    fun contains(plateId: Plate.ID): Boolean {
+    fun remove(plateId: Plate.ID): Boolean {
         if (plates == null)
             return false
-        return plates!!.contains(plateId)
+        return (plates as HashSet).remove(plateId)
     }
 
-    fun remove(plateId: Plate.ID) {
-        (plates as HashSet).remove(plateId)
+    override fun toString(): String {
+        var s = "$stop: "
+        if (plates == null)
+            s += "NULL"
+        else {
+            s += "{"
+            s += plates!!.joinToString { it.toString() }
+            s += "}"
+        }
+        return s
     }
 
     val size: Int




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
index ceed419ddda0ed2bca34443399301c56f5818792..3903ffbcec24fc67e2067efcf815c75e40d1545d 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
@@ -4,6 +4,9 @@ import android.annotation.SuppressLint
 import android.content.Context
 import android.database.*
 import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteException
+import android.util.SparseArray
+import android.util.SparseBooleanArray
 import ml.adamsprogs.bimba.*
 import ml.adamsprogs.bimba.models.gtfs.*
 import ml.adamsprogs.bimba.models.suggestions.*
@@ -37,53 +40,54 @@         private fun constructTimetable(context: Context) {
             val timetable = Timetable()
             val filesDir = context.getSecondaryExternalFilesDir()
             val dbFile = File(filesDir, "timetable.db")
-            timetable.db = SQLiteDatabase.openDatabase(dbFile.path, null, SQLiteDatabase.OPEN_READONLY) // fixme can be null
+            timetable.db = try {
+                SQLiteDatabase.openDatabase(dbFile.path, null, SQLiteDatabase.OPEN_READONLY)
+            } catch (e: SQLiteException) {
+                null
+            }
             this.timetable = timetable
         }
     }
 
-    private lateinit var db: SQLiteDatabase
+    private var db: SQLiteDatabase? = null
     private var _stops: List<StopSuggestion>? = null
 
     fun refresh() {
     }
 
-    fun getStopSuggestions(context: Context, force: Boolean = false): List<StopSuggestion> {
+    fun getStopSuggestions(/*context: Context, */force: Boolean = false): List<StopSuggestion> {
         if (_stops != null && !force)
             return _stops!!
 
-        val ids = HashMap<String, HashSet<AgencyAndId>>()
         val zones = HashMap<String, String>()
 
-        val cursor = db.rawQuery("select stop_name, stop_id, zone_id from stops", null)
+        val cursor = db!!.rawQuery("select stop_name, zone_id from stops", null)
 
         while (cursor.moveToNext()) {
             val name = cursor.getString(0)
-            val id = cursor.getInt(1)
-            val zone = cursor.getString(2)
-            if (name !in ids)
-                ids[name] = HashSet()
-            ids[name]!!.add(AgencyAndId(id.toString()))
+            val zone = cursor.getString(1)
             zones[name] = zone
         }
 
         cursor.close()
 
-        _stops = ids.map {
+        _stops = zones.map {
+            /*todo
             val colour = when (zones[it.key]) {
                 "A" -> "#${getColour(R.color.zoneA, context).toString(16)}"
                 "B" -> "#${getColour(R.color.zoneB, context).toString(16)}"
                 "C" -> "#${getColour(R.color.zoneC, context).toString(16)}"
                 else -> "#000000"
             }
-            StopSuggestion(it.key, it.value, zones[it.key]!!, colour)
+            */
+            StopSuggestion(it.key, it.value, "#000000")
         }.sorted()
         return _stops!!
     }
 
     fun getLineSuggestions(): List<LineSuggestion> {
         val routes = ArrayList<LineSuggestion>()
-        val cursor = db.rawQuery("select * from routes", null)
+        val cursor = db!!.rawQuery("select * from routes", null)
 
         while (cursor.moveToNext()) {
             val routeId = cursor.getString(0)
@@ -95,31 +99,35 @@
         return routes.sortedBy { it.name }
     }
 
-    fun getHeadlinesForStop(stops: Set<AgencyAndId>): Map<AgencyAndId, Pair<String, Set<String>>> {
-        val headsigns = HashMap<AgencyAndId, Pair<String, HashSet<String>>>()
+    fun getHeadlinesForStop(stop: String): Map<String, Set<String>> {
+        val headsigns = HashMap<String, HashSet<String>>()
 
-        val stopsIndex = HashMap<Int, String>()
-        val where = stops.joinToString(" or ", "where ") { "stop_id = ?" }
-        var cursor = db.rawQuery("select stop_id, stop_code from stops $where", stops.map { it.toString() }.toTypedArray())
-
+        var cursor = db!!.rawQuery("select stop_id, stop_code from stops where stop_name = ?",
+                arrayOf(stop))
+        val stopIds = ArrayList<String>()
+        val stopCodes = SparseArray<String>()
         while (cursor.moveToNext()) {
-            stopsIndex[cursor.getInt(0)] = cursor.getString(1)
+            cursor.getInt(0).let {
+                stopIds.add(it.toString())
+                stopCodes.put(it, cursor.getString(1))
+            }
         }
 
         cursor.close()
 
-        cursor = db.rawQuery("select stop_id, route_id, trip_headsign " +
+        val where = stopIds.joinToString(" or ", "where ") { "stop_id = ?" }
+
+        cursor = db!!.rawQuery("select stop_id, route_id, trip_headsign " +
                 "from stop_times natural join trips " +
-                where, stops.map { it.toString() }.toTypedArray())
+                where, stopIds.toTypedArray())
 
         while (cursor.moveToNext()) {
-            val stop = cursor.getInt(0)
-            val stopId = AgencyAndId(stop.toString())
+            val stopCode = stopCodes[cursor.getInt(0)]
             val route = cursor.getString(1)
             val headsign = cursor.getString(2)
-            if (stopId !in headsigns)
-                headsigns[stopId] = Pair(stopsIndex[stop]!!, HashSet())
-            headsigns[stopId]!!.second.add("$route → $headsign")
+            if (stopCode !in headsigns)
+                headsigns[stopCode] = HashSet()
+            headsigns[stopCode]!!.add("$route → $headsign")
         }
 
         cursor.close()
@@ -127,19 +135,42 @@
         return headsigns
 
         /*
-        1435 -> (AWF03, {232 → Os. Rusa})
-        1436 -> (AWF04, {232 → Rondo Kaponiera})
-        1437 -> (AWF02, {76 → Pl. Bernardyński, 74 → Os. Sobieskiego, 603 → Pl. Bernardyński})
-        1634 -> (AWF01, {76 → Os. Dębina, 603 → Łęczyca/Dworcowa})
-        171 -> (AWF42, {29 → Pl. Wiosny Ludów})
-        172 -> (AWF41, {10 → Połabska, 29 → Dębiec, 15 → Budziszyńska, 10 → Dębiec, 15 → Os. Sobieskiego, 12 → Os. Sobieskiego, 6 → Junikowo, 18 → Ogrody, 2 → Ogrody})
-        4586 -> (AWF73, {10 → Franowo, 29 → Franowo, 6 → Miłostowo, 5 → Stomil, 18 → Franowo, 15 → Franowo, 12 → Starołęka, 74 → Os. Orła Białego})
+        AWF03 -> {232 → Os. Rusa}
+        AWF04 -> {232 → Rondo Kaponiera}
+        AWF02 -> {76 → Pl. Bernardyński, 74 → Os. Sobieskiego, 603 → Pl. Bernardyński}
+        AWF01 ->{76 → Os. Dębina, 603 → Łęczyca/Dworcowa}
+        AWF42 -> {29 → Pl. Wiosny Ludów}
+        AWF41 -> {10 → Połabska, 29 → Dębiec, 15 → Budziszyńska, 10 → Dębiec, 15 → Os. Sobieskiego, 12 → Os. Sobieskiego, 6 → Junikowo, 18 → Ogrody, 2 → Ogrody}
+        AWF73 -> {10 → Franowo, 29 → Franowo, 6 → Miłostowo, 5 → Stomil, 18 → Franowo, 15 → Franowo, 12 → Starołęka, 74 → Os. Orła Białego}
         */
     }
 
-    fun getStopName(stopId: AgencyAndId): String {
-        val cursor = db.rawQuery("select stop_name from stops where stop_id = ?",
-                arrayOf(stopId.id))
+    fun getHeadlinesForStopCode(stop: String): StopSegment {
+        var cursor = db!!.rawQuery("select stop_id from stops where stop_code = ?",
+                arrayOf(stop))
+        cursor.moveToFirst()
+        val stopId = cursor.getInt(0)
+        cursor.close()
+
+
+        cursor = db!!.rawQuery("select route_id, trip_headsign " +
+                "from stop_times natural join trips where stop_id = ? ",
+                arrayOf(stopId.toString()))
+
+        val plates = HashSet<Plate.ID>()
+
+        while (cursor.moveToNext()) {
+            val route = cursor.getString(0)
+            val headsign = cursor.getString(1)
+            plates.add(Plate.ID(route, stop, headsign))
+        }
+        cursor.close()
+        return StopSegment(stop, plates)
+    }
+
+    fun getStopName(stopCode: String): String {
+        val cursor = db!!.rawQuery("select stop_name from stops where stop_code = ?",
+                arrayOf(stopCode))
         cursor.moveToNext()
         val name = cursor.getString(0)
         cursor.close()
@@ -147,9 +178,19 @@
         return name
     }
 
-    fun getStopCode(stopId: AgencyAndId): String {
-        val cursor = db.rawQuery("select stop_code from stops where stop_id = ?",
-                arrayOf(stopId.id))
+    fun getStopId(stopCode: String): String {
+        val cursor = db!!.rawQuery("select stop_id from stops where stop_code = ?",
+                arrayOf(stopCode))
+        cursor.moveToNext()
+        val id = cursor.getString(0)
+        cursor.close()
+
+        return id
+    }
+
+    fun getStopCode(stopId: String): String {
+        val cursor = db!!.rawQuery("select stop_code from stops where stop_id = ?",
+                arrayOf(stopId))
         cursor.moveToNext()
         val code = cursor.getString(0)
         cursor.close()
@@ -157,16 +198,17 @@
         return code
     }
 
-    fun getStopDepartures(stopId: AgencyAndId): Map<AgencyAndId, List<Departure>> {
-        val map = HashMap<AgencyAndId, ArrayList<Departure>>()
-        val cursor = db.rawQuery("select route_id, service_id, departure_time, " +
+    fun getStopDepartures(stopCode: String): Map<String, List<Departure>> {
+        val stopID = getStopId(stopCode)
+        val map = HashMap<String, ArrayList<Departure>>()
+        val cursor = db!!.rawQuery("select route_id, service_id, departure_time, " +
                 "wheelchair_accessible, stop_sequence, trip_id, trip_headsign, route_desc " +
                 "from stop_times natural join trips natural join routes where stop_id = ?",
-                arrayOf(stopId.id))
+                arrayOf(stopID))
 
         while (cursor.moveToNext()) {
-            val line = AgencyAndId(cursor.getString(0))
-            val service = AgencyAndId(cursor.getInt(1).toString())
+            val line = cursor.getString(0)
+            val service = cursor.getInt(1).toString()
             val mode = calendarToMode(service)
             val time = parseTime(cursor.getString(2))
             val lowFloor = cursor.getInt(3) == 1
@@ -190,14 +232,21 @@
         return map
     }
 
-    fun getStopDeparturesBySegments(segments: HashSet<StopSegment>): Map<AgencyAndId, List<Departure>> {
+    fun getStopDeparturesBySegments(segments: Set<StopSegment>): Map<String, List<Departure>> {
+        val stopCodes = HashMap<String, Int>()
+        var cursor = db!!.rawQuery("select stop_id, stop_code from stops", emptyArray())
+        while (cursor.moveToNext()) {
+            stopCodes[cursor.getString(1)] = cursor.getInt(0)
+        }
+        cursor.close()
+
         val wheres = segments.flatMap {
-            it.plates?.map {
-                "(stop_id = ${it.stop} and route_id = '${it.line}' and trip_headsign = '${it.headsign}')"
-            } ?: listOf()
+            it.plates?.map { plate ->
+                "(stop_id = ${stopCodes[plate.stop]} and route_id = '${plate.line}' and trip_headsign = '${plate.headsign}')"
+            } ?: listOf("stop_id = ${stopCodes[it.stop]}")
         }.joinToString(" or ")
 
-        val cursor = db.rawQuery("select route_id, service_id, departure_time, " +
+        cursor = db!!.rawQuery("select route_id, service_id, departure_time, " +
                 "wheelchair_accessible, stop_sequence, trip_id, trip_headsign, route_desc " +
                 "from stop_times natural join trips natural join routes where $wheres", null)
 
@@ -206,12 +255,12 @@         cursor.close()
         return map
     }
 
-    private fun parseDeparturesCursor(cursor: Cursor): Map<AgencyAndId, List<Departure>> {
-        val map = HashMap<AgencyAndId, ArrayList<Departure>>()
+    private fun parseDeparturesCursor(cursor: Cursor): Map<String, List<Departure>> {
+        val map = HashMap<String, ArrayList<Departure>>()
 
         while (cursor.moveToNext()) {
-            val line = AgencyAndId(cursor.getString(0))
-            val service = AgencyAndId(cursor.getInt(1).toString())
+            val line = cursor.getString(0)
+            val service = cursor.getInt(1).toString()
             val mode = calendarToMode(service)
             val time = parseTime(cursor.getString(2))
             val lowFloor = cursor.getInt(3) == 1
@@ -243,10 +292,10 @@         cal.set(JCalendar.SECOND, s.toInt())
         return cal.secondsAfterMidnight()
     }
 
-    fun calendarToMode(serviceId: AgencyAndId): List<Int> {
+    private fun calendarToMode(serviceId: String): List<Int> {
         val days = ArrayList<Int>()
-        val cursor = db.rawQuery("select * from calendar where service_id = ?",
-                arrayOf(serviceId.id))
+        val cursor = db!!.rawQuery("select * from calendar where service_id = ?",
+                arrayOf(serviceId))
 
         cursor.moveToNext()
         (1 until 7).forEach {
@@ -262,9 +311,9 @@         val explanations = ArrayList()
         tripId.modification.forEach {
             if (it.stopRange != null) {
                 if (stopSequence in it.stopRange)
-                    explanations.add(routeModifications[it.id.id]!!)
+                    explanations.add(routeModifications[it.id]!!)
             } else {
-                explanations.add(routeModifications[it.id.id]!!)
+                explanations.add(routeModifications[it.id]!!)
             }
         }
 
@@ -295,23 +344,25 @@             if (modification != "") {
                 modification.split(",").forEach {
                     try {
                         val (id, start, end) = it.split(":")
-                        modifications.add(Trip.ID.Modification(AgencyAndId(id), IntRange(start.toInt(), end.toInt())))
+                        modifications.add(Trip.ID.Modification(id, IntRange(start.toInt(), end.toInt())))
                     } catch (e: Exception) {
-                        modifications.add(Trip.ID.Modification(AgencyAndId(it), null))
+                        modifications.add(Trip.ID.Modification(it, null))
                     }
                 }
             }
-            return Trip.ID(rawId, AgencyAndId(rawId.split("^")[0]), modifications, isMain)
+            return Trip.ID(rawId, rawId.split("^")[0], modifications, isMain)
         } else
-            return Trip.ID(rawId, AgencyAndId(rawId), HashSet(), false)
+            return Trip.ID(rawId, rawId, HashSet(), false)
     }
 
     @SuppressLint("Recycle")
     fun isEmpty(): Boolean {
+        if (db == null)
+            return true
         var result: Boolean
         var cursor: Cursor? = null
         try {
-            cursor = db.rawQuery("select * from feed_info", null)
+            cursor = db!!.rawQuery("select * from feed_info", null)
             result = !cursor.moveToNext()
         } catch (e: Exception) {
             result = true
@@ -322,7 +373,7 @@         return result
     }
 
     fun getValidSince(): String {
-        val cursor = db.rawQuery("select feed_start_date from feed_info", null)
+        val cursor = db!!.rawQuery("select feed_start_date from feed_info", null)
 
         cursor.moveToNext()
         val validTill = cursor.getString(0)
@@ -332,7 +383,7 @@         return validTill
     }
 
     fun getValidTill(): String {
-        val cursor = db.rawQuery("select feed_end_date from feed_info", null)
+        val cursor = db!!.rawQuery("select feed_end_date from feed_info", null)
 
         cursor.moveToNext()
         val validTill = cursor.getString(0)
@@ -341,41 +392,42 @@         cursor.close()
         return validTill
     }
 
-    fun getServiceForToday(): AgencyAndId {
-        val today = JCalendar.getInstance().get(JCalendar.DAY_OF_WEEK)
+    fun getServiceForToday(): String? {
+        val today = JCalendar.getInstance()
         return getServiceFor(today)
     }
 
-    fun getServiceForTomorrow(): AgencyAndId {
+    fun getServiceForTomorrow(): String? {
         val tomorrow = JCalendar.getInstance()
         tomorrow.add(JCalendar.DAY_OF_MONTH, 1)
-        val tomorrowDoW = tomorrow.get(JCalendar.DAY_OF_WEEK)
-        return getServiceFor(tomorrowDoW)
+        return getServiceFor(tomorrow)
     }
 
-    fun getServiceFor(day: Int): AgencyAndId {
-        val dayColumn = arrayOf("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")[((day + 5) % 7)]
-        val cursor = db.rawQuery("select service_id from calendar where $dayColumn = 1", null)
+    private fun getServiceFor(day: JCalendar): String? {
+        val dayColumn = arrayOf("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")[((day.get(JCalendar.DAY_OF_WEEK) + 5) % 7)]
+        val cursor = db!!.rawQuery("select service_id from calendar where $dayColumn = 1 and start_date < ? and ? < end_date", arrayOf(day.toIsoDate(), day.toIsoDate()))
 
-        val service: Int
-        cursor.moveToNext()
-        try {
-            service = cursor.getInt(0)
+        cursor.moveToFirst()
+        return try {
+            cursor.getInt(0).let {
+                cursor.close()
+                it.toString()
+            }
+        } catch (e: CursorIndexOutOfBoundsException) {
             cursor.close()
-            return AgencyAndId(service.toString())
-        } catch (e: CursorIndexOutOfBoundsException) {
-            throw IllegalArgumentException()
+            null
         }
     }
 
-    fun getPlatesForStop(stop: AgencyAndId): Set<Plate.ID> {
+    private fun getPlatesForStop(stop: String): Set<Plate.ID> {
+
         val plates = HashSet<Plate.ID>()
-        val cursor = db.rawQuery("select route_id, trip_headsign " +
-                "from stop_times natural join trips where stop_id = ? " +
-                "group by route_id, trip_headsign", arrayOf(stop.id))
+        val cursor = db!!.rawQuery("select route_id, trip_headsign " +
+                "from stop_times natural join trips natural join stops where stop_code = ? " +
+                "group by route_id, trip_headsign", arrayOf(stop))
 
         while (cursor.moveToNext()) {
-            val routeId = AgencyAndId(cursor.getString(0))
+            val routeId = cursor.getString(0)
             val headsign = cursor.getString(1)
             plates.add(Plate.ID(routeId, stop, headsign))
         }
@@ -384,13 +436,13 @@         cursor.close()
         return plates
     }
 
-    fun getTripGraphs(id: AgencyAndId): Array<TripGraph> {
+    fun getTripGraphs(id: String): Array<TripGraph> {
         val graphs = arrayOf(TripGraph(), TripGraph())
 
-        val cursor = db.rawQuery("select trip_id, trip_headsign, direction_id, stop_id, " +
+        val cursor = db!!.rawQuery("select trip_id, trip_headsign, direction_id, stop_id, " +
                 "stop_sequence, pickup_type, stop_name, zone_id " +
                 "from stop_times natural join trips natural join stops" +
-                "where route_id = ?", arrayOf(id.id))
+                "where route_id = ?", arrayOf(id))
 
         while (cursor.moveToNext()) {
             val trip = cursor.getString(0)
@@ -435,6 +487,56 @@             }
         }
 
         return graphs
+    }
+
+    fun getServiceFirstDay(service: String): Int {
+        val cursor = db!!.rawQuery("select * from calendar where service_id = ?", arrayOf(service))
+        cursor.moveToFirst()
+        var i = 1
+        while ((cursor.getString(i) == "0") and (i < 8)) i++
+        cursor.close()
+        return i
+    }
+
+    fun getServiceDescription(service: String, context: Context): String {
+        val dayNames = SparseArray<String>()
+        dayNames.put(1, context.getString(R.string.Mon))
+        dayNames.put(2, context.getString(R.string.Tue))
+        dayNames.put(3, context.getString(R.string.Wed))
+        dayNames.put(4, context.getString(R.string.Thu))
+        dayNames.put(5, context.getString(R.string.Fri))
+        dayNames.put(6, context.getString(R.string.Sat))
+        dayNames.put(7, context.getString(R.string.Sun))
+
+        val cursor = db!!.rawQuery("select * from calendar where service_id = ?", arrayOf(service))
+        cursor.moveToFirst()
+        val days = SparseBooleanArray()
+        for (i in 1..7) {
+            days.append(i, cursor.getString(i) == "1")
+        }
+        days.append(8, false)
+        val description = ArrayList<String>()
+        var start = 0
+
+        for (i in 1..8) {
+            if (!days[i] and (start > 0)) {
+                when {
+                    i - start == 1 -> description.add(dayNames[start])
+                    i - start == 2 -> description.add("${dayNames[start]}, ${dayNames[start + 1]}")
+                    i - start > 2 -> description.add("${dayNames[start]}–${dayNames[i - 1]}")
+                }
+                start = 0
+            }
+            if (days[i] and (start == 0))
+                start = i
+        }
+
+        val startDate = calendarFromIsoD(cursor.getString(8)).toNiceString(context)
+        val endDate = calendarFromIsoD(cursor.getString(9)).toNiceString(context)
+
+        cursor.close()
+
+        return "${description.joinToString { it }} ($startDate–$endDate)"
     }
 
     class TripGraph {




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
index 13a2f5765c5ec92fd13aa9ec8f05681c5f3ce0ed..c8915edf9e1b3e8b8444f3661e989ce6f3dc2810 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt
@@ -16,7 +16,7 @@ import ml.adamsprogs.bimba.models.Departure
 import ml.adamsprogs.bimba.rollTime
 import java.util.*
 
-class DeparturesAdapter(val context: Context, private val departures: List<Departure>?, private val relativeTime: Boolean) :
+class DeparturesAdapter(val context: Context, var departures: List<Departure>?, var relativeTime: Boolean) :
         RecyclerView.Adapter<DeparturesAdapter.ViewHolder>() {
 
     companion object {
@@ -26,31 +26,32 @@         const val VIEW_TYPE_EMPTY: Int = 2
     }
 
     override fun getItemCount(): Int {
-        if (departures == null || departures.isEmpty())
+        if (departures == null || departures!!.isEmpty())
             return 1
-        return departures.size
+        return departures!!.size
     }
 
     override fun getItemViewType(position: Int): Int {
         return when {
-            departures == null -> VIEW_TYPE_EMPTY
-            departures.isEmpty() -> VIEW_TYPE_LOADING
+            departures == null -> VIEW_TYPE_LOADING //empty
+            departures!!.isEmpty() -> VIEW_TYPE_LOADING
             else -> VIEW_TYPE_CONTENT
         }
     }
 
     override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        // todo migotanie ikon
         if (departures == null) {
             return
         }
         val line = holder.lineTextView
         val time = holder.timeTextView
         val direction = holder.directionTextView
-        if (departures.isEmpty()) {
+        if (departures!!.isEmpty()) {
             time.text = context.getString(R.string.no_departures)
             return
         }
-        val departure = departures[position]
+        val departure = departures!![position]
         val now = Calendar.getInstance()
         val departureTime = Calendar.getInstance().rollTime(departure.time)
         if (departure.tomorrow)




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
index 6814ada2b4f7f4e9ff219337c1a4e1e03c643fae..d9a29e8872e4a7f99c68c282ef20cdf823791fb4 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt
@@ -6,29 +6,60 @@ import android.view.View
 import android.view.ViewGroup
 import android.widget.ImageView
 import android.widget.TextView
+import kotlinx.coroutines.experimental.DefaultDispatcher
+import kotlinx.coroutines.experimental.android.UI
+import kotlinx.coroutines.experimental.launch
+import kotlinx.coroutines.experimental.withContext
+import ml.adamsprogs.bimba.ProviderProxy
 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
+import ml.adamsprogs.bimba.models.StopSegment
+
 
 class FavouriteEditRowAdapter(private var favourite: Favourite) :
         RecyclerView.Adapter<FavouriteEditRowAdapter.ViewHolder>() {
+
+    private val segments = HashMap<String, StopSegment>()
+    private val providerProxy = ProviderProxy()
+
+    init {
+        launch(UI) {
+            withContext(DefaultDispatcher) {
+                favourite.segments.forEach {
+                    segments[it.stop] = providerProxy.fillStopSegment(it) ?: it
+                }
+            }
+            this@FavouriteEditRowAdapter.notifyDataSetChanged()
+        }
+    }
+
+
     override fun getItemCount(): Int {
-        return favourite.size
+        return segments.flatMap { it.value.plates ?: emptyList<Plate.ID>() }.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.deleteButton.setOnClickListener {
-            favourites.delete(favourite.name, id)
-            favourite = favourites.favourites[favourite.name]!!
-            notifyDataSetChanged()
+        launch(UI) {
+            val plates = segments.flatMap { it.value.plates ?: emptyList<Plate.ID>() }
+            val favourites = FavouriteStorage.getFavouriteStorage()
+            val id = plates.sortedBy { "${it.line}${it.stop}" }[position]
+            val favouriteElement = withContext(DefaultDispatcher) {
+                providerProxy.getStopName(id.stop).let {
+                    "${it ?: ""} (${id.stop}):\n${id.line} → ${id.headsign}"
+                }
+            }
+            holder.rowTextView.text = favouriteElement
+            holder.deleteButton.setOnClickListener {
+                launch(UI) {
+                    favourite.segments.clear()
+                    favourite.segments.addAll(segments.map { it.value })
+                    favourites.delete(favourite.name, id)
+                    favourite = favourites.favourites[favourite.name]!!
+                    notifyDataSetChanged()
+                }
+            }
         }
     }
 
@@ -41,7 +72,7 @@         return ViewHolder(rowView)
     }
 
     inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
-        val rowTextView:TextView = itemView.findViewById(R.id.favourite_edit_row)
-        val deleteButton:ImageView = itemView.findViewById(R.id.favourite_edit_delete)
+        val rowTextView: TextView = itemView.findViewById(R.id.favourite_edit_row)
+        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
index 6cef0867376e4bcf1b8aecf4df91fc3b683ede7f..76206a2b5e93f9683ff8dc07aa7011fa48c2a29e 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt
@@ -9,10 +9,8 @@ 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 kotlinx.coroutines.experimental.*
 import java.util.*
 import ml.adamsprogs.bimba.Declinator
 import ml.adamsprogs.bimba.collections.FavouriteStorage
@@ -61,7 +59,7 @@             val favourite = favourites[position]!!
             holder.nameTextView.text = favourite.name
 
             holder.selectedOverlay.visibility = if (isSelected(position)) View.VISIBLE else View.INVISIBLE
-            holder.moreButton.setOnClickListener {
+            holder.moreButton.setOnClickListener { it ->
                 val popup = PopupMenu(appContext, it)
                 val inflater = popup.menuInflater
                 popup.setOnMenuItemClickListener {
@@ -75,9 +73,9 @@                 inflater.inflate(R.menu.favourite_actions, popup.menu)
                 popup.show()
             }
 
-            val nextDeparture = async(CommonPool) {
+            val nextDeparture = withContext(CommonPool) {
                 favourite.nextDeparture()
-            }.await()
+            }
 
             val nextDepartureText: String
             val nextDepartureLineText: String




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/ServiceAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/ServiceAdapter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..014cd4a7cf014ff56ec0ca3e54a77c90a769f805
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/ServiceAdapter.kt
@@ -0,0 +1,43 @@
+package ml.adamsprogs.bimba.models.adapters
+
+import android.annotation.SuppressLint
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import android.view.LayoutInflater
+import ml.adamsprogs.bimba.R
+import android.app.Activity
+import android.widget.ArrayAdapter
+import ml.adamsprogs.bimba.ProviderProxy
+
+
+class ServiceAdapter(context: Activity, resourceId: Int, list: List<RowItem>) : ArrayAdapter<ServiceAdapter.RowItem>(context, resourceId, list) {
+
+    private val inflater: LayoutInflater = context.layoutInflater
+
+    @SuppressLint("ViewHolder", "InflateParams")
+    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+        val rowItem: RowItem = getItem(position)
+        val rowView = inflater.inflate(R.layout.toolbar_spinner_item, null, true)
+        rowView.findViewById<TextView>(R.id.text).text = rowItem.description
+
+        return rowView
+    }
+
+    @SuppressLint("InflateParams")
+    override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup?): View {
+        val rowItem: RowItem = getItem(position)
+        val rowView = inflater.inflate(R.layout.toolbar_spinner_item, null, true)
+        rowView.findViewById<TextView>(R.id.text).text = rowItem.description
+
+        return rowView
+
+    }
+
+    data class RowItem(val service: String, val description: String) : Comparable<RowItem> {
+        override fun compareTo(other: RowItem): Int {
+            val proxy = ProviderProxy()
+            return proxy.getServiceFirstDay(service).compareTo(proxy.getServiceFirstDay(other.service))
+        }
+    }
+}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/AgencyAndId.kt b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/AgencyAndId.kt
deleted file mode 100644
index 66c8d620d3dfbcd85c3def1d14d5cfee300e878e..0000000000000000000000000000000000000000
--- a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/AgencyAndId.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package ml.adamsprogs.bimba.models.gtfs
-
-import java.io.Serializable
-
-data class AgencyAndId(val id: String) : Serializable, Comparable<AgencyAndId> {
-    override fun compareTo(other: AgencyAndId): Int {
-        return this.toString().compareTo(other.toString())
-    }
-
-    companion object {
-        fun convertFromString(str: String): AgencyAndId {
-            return AgencyAndId(str)
-        }
-    }
-
-    override fun toString(): String {
-        return id
-    }
-}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Route.kt b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Route.kt
index ddba5daf28fd737a6c067c263d10c19ecc9d05d0..c3021d8a3c0991b4cee3150a5a2f9751a2ea8764 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Route.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Route.kt
@@ -4,7 +4,7 @@ import android.os.Parcel
 import android.os.Parcelable
 
 
-data class Route(val id: AgencyAndId, val agency: AgencyAndId, val shortName: String,
+data class Route(val id: String, val agency: String, val shortName: String,
                  val longName: String, val description: String, val type: Int, val colour: Int,
                  val textColour: Int, val modifications: Map<String, String>) : Parcelable {
     companion object CREATOR : Parcelable.Creator<Route> {
@@ -27,13 +27,13 @@                 val fromSplit = from.split("^")
                 val toSplit = to.split("^")
                 val description = "${toSplit[0]}|${fromSplit[0]}"
                 val modifications = createModifications(desc)
-                Route(AgencyAndId(id), AgencyAndId(agency), shortName, longName, description,
+                Route(id, agency, shortName, longName, description,
                         type, colour, textColour, modifications)
             } else {
                 val toSplit = desc.split("^")
                 val description = toSplit[0]
                 val modifications = createModifications(desc)
-                Route(AgencyAndId(id), AgencyAndId(agency), shortName, longName, description,
+                Route(id, agency, shortName, longName, description,
                         type, colour, textColour, modifications)
             }
         }
@@ -57,8 +57,8 @@     }
 
     @Suppress("UNCHECKED_CAST")
     constructor(parcel: Parcel) : this(
-            AgencyAndId(parcel.readString()),
-            AgencyAndId(parcel.readString()),
+            parcel.readString(),
+            parcel.readString(),
             parcel.readString(),
             parcel.readString(),
             parcel.readString(),
@@ -68,8 +68,8 @@             parcel.readInt(),
             parcel.readSerializable() as HashMap<String, String>)
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
-        parcel.writeString(id.id)
-        parcel.writeString(agency.id)
+        parcel.writeString(id)
+        parcel.writeString(agency)
         parcel.writeString(shortName)
         parcel.writeString(longName)
         parcel.writeString(description)




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Trip.kt b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Trip.kt
index 6e2de9c4c35d16fd71e0a31745ee277dc5947134..db797a31de4ba22531ebe14266ed73ed38e72254 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Trip.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Trip.kt
@@ -1,9 +1,9 @@
 package ml.adamsprogs.bimba.models.gtfs
 
-data class Trip(val routeId: AgencyAndId, val serviceId: AgencyAndId, val id: ID,
-                val headsign: String, val direction: Int, val shapeId: AgencyAndId,
+data class Trip(val routeId: String, val serviceId: String, val id: ID,
+                val headsign: String, val direction: Int, val shapeId: String,
                 val wheelchairAccessible: Boolean) {
-    data class ID(val rawId:String, val id: AgencyAndId, val modification: Set<Modification>, val isMain: Boolean) {
-        data class Modification(val id: AgencyAndId, val stopRange: IntRange?)
+    data class ID(val rawId:String, val id: String, val modification: Set<Modification>, val isMain: Boolean) {
+        data class Modification(val id: String, val stopRange: IntRange?)
     }
 }
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/suggestions/StopSuggestion.kt b/app/src/main/java/ml/adamsprogs/bimba/models/suggestions/StopSuggestion.kt
index 934a6e0e2ef25631fe6fde8162becd1260b8a8d8..cd3ef7762cb9f0e16a905cba2df8193e9d8b4568 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/suggestions/StopSuggestion.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/suggestions/StopSuggestion.kt
@@ -3,11 +3,10 @@
 import android.os.Parcel
 import android.os.Parcelable
 import ml.adamsprogs.bimba.R
-import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
 
-class StopSuggestion(name: String, val ids: Set<AgencyAndId>, private val zone: String, private val zoneColour: String) : GtfsSuggestion(name){
+class StopSuggestion(name: String, private val zone: String, private val zoneColour: String) : GtfsSuggestion(name){
     @Suppress("UNCHECKED_CAST")
-    constructor(parcel: Parcel) : this(parcel.readString(), parcel.readString().split(",").map { AgencyAndId(it) }.toSet(), parcel.readString(), parcel.readString())
+    constructor(parcel: Parcel) : this(parcel.readString(), parcel.readString(), parcel.readString())
 
     override fun describeContents(): Int {
         return Parcelable.CONTENTS_FILE_DESCRIPTOR
@@ -15,7 +14,6 @@     }
 
     override fun writeToParcel(dest: Parcel?, flags: Int) {
         dest?.writeString(name)
-        dest?.writeString(ids.joinToString(",") { it.toString() })
         dest?.writeString(zone)
         dest?.writeString(zoneColour)
     }




diff --git a/app/src/main/res/drawable/icon_dev.xml b/app/src/main/res/drawable/icon_dev.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a4b238053d6fdefaa6c74cfe50a206f56b769aff
--- /dev/null
+++ b/app/src/main/res/drawable/icon_dev.xml
@@ -0,0 +1,17 @@
+<vector android:height="24dp" android:viewportHeight="293"
+    android:viewportWidth="298" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillAlpha="1" android:fillColor="#5E5E5E"
+        android:pathData="m62.925,292.677v0c-3.69,-0.897 -5.684,-3.789 -4.288,-6.482L90.846,223.472c1.296,-2.593 5.485,-3.989 9.174,-2.992v0c3.69,0.897 5.684,3.789 4.288,6.482l-32.209,62.723c-1.296,2.593 -5.385,3.989 -9.174,2.992z" android:strokeWidth="0.99718279"/>
+    <path android:fillAlpha="1" android:fillColor="#5E5E5E"
+        android:pathData="m229.156,292.677v0c3.69,-0.897 5.684,-3.789 4.288,-6.482l-32.209,-62.723c-1.296,-2.593 -5.485,-3.989 -9.174,-2.992v0c-3.69,0.897 -5.684,3.789 -4.288,6.482l32.209,62.723c1.296,2.593 5.485,3.989 9.174,2.992z" android:strokeWidth="0.99718279"/>
+    <path android:fillAlpha="1" android:fillColor="#5E5E5E"
+        android:pathData="m151.076,37.797 l-0.598,0.199c-1.795,0.698 -3.789,-0.299 -4.487,-2.094L136.418,9.277c-0.698,-1.795 0.299,-3.789 2.094,-4.487l0.598,-0.199c1.795,-0.698 3.789,0.299 4.487,2.094l9.573,26.625c0.698,1.895 -0.299,3.889 -2.094,4.487z" android:strokeWidth="0.99718279"/>
+    <path android:fillAlpha="1" android:fillColor="#5E5E5E"
+        android:pathData="m180.294,4.79v0c0,2.094 -1.695,3.789 -3.789,3.789h-60.828c-2.094,0 -3.789,-1.695 -3.789,-3.789v0c0,-2.094 1.695,-3.789 3.789,-3.789h60.828c2.094,0 3.789,1.695 3.789,3.789z" android:strokeWidth="0.99718279"/>
+    <path android:fillAlpha="1" android:fillColor="#5E5E5E"
+        android:pathData="M218.286,237.034H73.795c-13.263,0 -23.932,-10.77 -23.932,-23.932V105.406c0,-41.184 33.406,-74.689 74.689,-74.689h43.178c41.184,0 74.689,33.406 74.689,74.689v107.696c-0.1,13.163 -10.869,23.932 -24.132,23.932z" android:strokeWidth="0.99718279"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M212.104,146.789L79.977,146.789c-5.584,0 -10.171,-4.487 -10.171,-10.171l0,-34.802c0,-16.852 13.661,-30.514 30.514,-30.514L191.761,71.302c16.852,0 30.514,13.661 30.514,30.514l0,34.802c-0.1,5.684 -4.587,10.171 -10.171,10.171z"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M161.148,56.344L130.933,56.344c-3.191,0 -5.684,-2.593 -5.684,-5.684l0,0c0,-3.191 2.593,-5.684 5.684,-5.684l30.215,0c3.191,0 5.684,2.593 5.684,5.684l0,0c0,3.091 -2.593,5.684 -5.684,5.684z"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M86.957,192.36m-14.758,0a14.758,14.758 0,1 1,29.517 0a14.758,14.758 0,1 1,-29.517 0"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M205.223,192.36m-14.758,0a14.758,14.758 0,1 1,29.517 0a14.758,14.758 0,1 1,-29.517 0"/>
+</vector>




diff --git a/app/src/main/res/layout/activity_stop.xml b/app/src/main/res/layout/activity_stop.xml
index cad4b10f71491458bd9cc388e9db098c409a41da..d6a0b92bfbdd7ad1d7be8b4b6481ca929b573770 100644
--- a/app/src/main/res/layout/activity_stop.xml
+++ b/app/src/main/res/layout/activity_stop.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/stop_layout"
@@ -7,6 +7,7 @@     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:fitsSystemWindows="true"
     tools:context="ml.adamsprogs.bimba.activities.StopActivity">
+
 
     <android.support.design.widget.AppBarLayout
         android:id="@+id/appbar"
@@ -27,36 +28,29 @@             app:title="@string/app_name">
 
         </android.support.v7.widget.Toolbar>
 
-        <android.support.design.widget.TabLayout
-            android:id="@+id/tabs"
+        <Spinner
+            android:id="@+id/dateSpinner"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content">
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="8dp"
+            android:layout_marginEnd="8dp"
+            android:layout_marginLeft="8dp"
+            android:layout_marginRight="8dp"
+            android:layout_marginStart="8dp"
+            android:layout_weight="1"
+            android:visibility="gone" />
 
-            <android.support.design.widget.TabItem
-                android:id="@+id/tabItem"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/tab_workday_text" />
-
-            <android.support.design.widget.TabItem
-                android:id="@+id/tabItem4"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/tab_saturday_text" />
 
-            <android.support.design.widget.TabItem
-                android:id="@+id/tabItem5"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/tab_sunday_text" />
-        </android.support.design.widget.TabLayout>
     </android.support.design.widget.AppBarLayout>
 
-    <android.support.v4.view.ViewPager
-        android:id="@+id/container"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/departuresList"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/appbar" />
 
     <android.support.design.widget.FloatingActionButton
         android:id="@+id/fab"
@@ -64,6 +58,9 @@         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="end|bottom"
         android:layout_margin="@dimen/fab_margin"
+        android:layout_marginBottom="16dp"
+        android:layout_marginEnd="16dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
         app:srcCompat="@drawable/ic_favourite" />
-
-</android.support.design.widget.CoordinatorLayout>
+</android.support.constraint.ConstraintLayout>




diff --git a/app/src/main/res/layout/fragment_stop.xml b/app/src/main/res/layout/fragment_stop.xml
deleted file mode 100644
index 4cadd826bbf25470cfb9f7417b425acadc0c9722..0000000000000000000000000000000000000000
--- a/app/src/main/res/layout/fragment_stop.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/constraintLayout"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    tools:context="ml.adamsprogs.bimba.activities.StopActivity$PlaceholderFragment">
-
-    <!-- todo landscape version -->
-    <android.support.v7.widget.RecyclerView
-        android:id="@+id/departuresList"
-        android:layout_width="368dp"
-        android:layout_height="516dp"
-        android:layout_marginBottom="8dp"
-        android:layout_marginStart="8dp"
-        android:layout_marginTop="8dp"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintVertical_bias="0.485"
-        app:layout_constraintEnd_toEndOf="parent"
-        android:layout_marginEnd="8dp" />
-
-</android.support.constraint.ConstraintLayout>
\ No newline at end of file




diff --git a/app/src/main/res/layout/toolbar_spinner_item.xml b/app/src/main/res/layout/toolbar_spinner_item.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1cb3890d88df2edaeec0cf238d7d7afb271c3f9c
--- /dev/null
+++ b/app/src/main/res/layout/toolbar_spinner_item.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/text"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:ellipsize="end"
+    android:singleLine="true"
+    android:textAlignment="inherit"
+    android:textColor="@color/text_on_toolbar" />




diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 9c139ca27b5819a8a45720dac6414ea95bab8fd7..5982e32ecdb6a838a1c7f45e3f82e9a77241302e 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -12,4 +12,5 @@     #ffc107
 
     <color name="tram">#00adef</color>
     <color name="bus">#c4212a</color>
+    <color name="text_on_toolbar">#ffffff</color>
 </resources>




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 59bae428d441c18d3f8b73074d7e738ee2d1c448..fc6cb018421f5b01bd650be84078c6b1522df076 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -90,4 +90,12 @@     key_timetable_source_url
     <string name="title_timetable_source_url">Timetable source</string>
     <string name="title_activity_settings">Settings</string>
     <string name="settings">Settings</string>
+
+    <string name="Mon">Mon</string>
+    <string name="Tue">Tue</string>
+    <string name="Wed">Wed</string>
+    <string name="Thu">Thu</string>
+    <string name="Fri">Fri</string>
+    <string name="Sat">Sat</string>
+    <string name="Sun">Sun</string>
 </resources>




diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 74a1403bc6cf2643facca4506c5f1b23cd015a27..bc754c49306b35a193dcb649178a347de04d8599 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -73,4 +73,11 @@     Quelle des Fahrplans
     <string name="title_activity_settings">Einstellungen</string>
     <string name="settings">Einstellungen</string>
     <string name="timetable_decompressing">Fahrplan wird entpackt</string>
+    <string name="Mon">Mo.</string>
+    <string name="Tue">Di.</string>
+    <string name="Wed">Mi.</string>
+    <string name="Thu">Do.</string>
+    <string name="Fri">Fr.</string>
+    <string name="Sat">Sa.</string>
+    <string name="Sun">So.</string>
 </resources>




diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index e18d6d65bf573b10800637ce426adbf393ea9bda..2f555258b4a51b199fde26df685ba07788e5bc20 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -71,4 +71,11 @@     Fonte dell’Orario
     <string name="title_activity_settings">Impostazioni</string>
     <string name="settings">Impostazioni</string>
     <string name="timetable_decompressing">Decomprimendo l’orario</string>
+    <string name="Mon">lun</string>
+    <string name="Tue">mar</string>
+    <string name="Wed">mer</string>
+    <string name="Thu">gio</string>
+    <string name="Fri">ven</string>
+    <string name="Sat">sab</string>
+    <string name="Sun">dom</string>
 </resources>




diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 3693fadac99d212054a413ec878bf37f093d5c77..82150b353f9a2b6c74446ff0d4415c6b8b4a12db 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -74,4 +74,11 @@     Bron van de dienstregeling
     <string name="title_activity_settings">Instellingen</string>
     <string name="settings">Instellingen</string>
     <string name="timetable_decompressing">Bezig met uitpakken van dienstregeling</string>
+    <string name="Mon">ma</string>
+    <string name="Tue">di</string>
+    <string name="Wed">wo</string>
+    <string name="Thu">do</string>
+    <string name="Fri">vr</string>
+    <string name="Sat">za</string>
+    <string name="Sun">zo</string>
 </resources>




diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 15370f10365138c4426f4c71c7ab5af1bc7370b0..ce9634e0554f906aa47db97f48b47415abfe3742 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -73,4 +73,11 @@     Źródło rozkładu
     <string name="title_activity_settings">Ustawienia</string>
     <string name="settings">Ustawienia</string>
     <string name="timetable_decompressing">Rozpakowywanie rozkładu</string>
+    <string name="Mon">pon.</string>
+    <string name="Tue">wt.</string>
+    <string name="Wed">śr.</string>
+    <string name="Thu">czw.</string>
+    <string name="Fri">pt.</string>
+    <string name="Sat">sob.</string>
+    <string name="Sun">niedz.</string>
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml
index 1c3647edb4ef835066979a1174d44dbd79359557..edbdd769a5229ecc162ac8bc3b128c86a91dbe4d 100644
--- a/app/src/main/res/xml/pref_main.xml
+++ b/app/src/main/res/xml/pref_main.xml
@@ -9,5 +9,10 @@             android:title="@string/title_timetable_source_url" />
 
         <!-- todo intent get file (import) -->
         <!-- todo reset source -->
+        <SwitchPreference
+            android:defaultValue="false"
+            android:key="key_timetable_automatic_update"
+            android:summary="Automatically check for and download timetable updates"
+            android:title="Automatic updates" />
     </PreferenceCategory>
 </PreferenceScreen>




diff --git a/build.gradle b/build.gradle
index abd71c41b0e3809a905d7e36ce37c4c9b60fb0b2..eb4b96ee435f6e9def2abe971a838aba0ac9b430 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,7 @@
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
 
 buildscript {
-    ext.kotlin_version = '1.2.51'
+    ext.kotlin_version = '1.2.60'
     repositories {
         jcenter()
         maven { url 'https://maven.google.com' }
@@ -9,7 +9,7 @@         //maven { url 'https://dl.bintray.com/guardian/android' } // TooLargeTool
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.1.3'
+        classpath 'com.android.tools.build:gradle:3.1.4'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 
         // NOTE: Do not place your application dependencies here; they belong




diff --git a/converter/local/converter.py b/converter/local/converter.py
index 8c817c00e9c0a965d79f605e37d95f2f8d3a94fc..842f7e79adce9f07473c6401dd332bb751cf1dff 100755
--- a/converter/local/converter.py
+++ b/converter/local/converter.py
@@ -87,6 +87,7 @@         s, e = name.split('_')
         return s + "{0:03}".format(100 - self.__validity_length(s, e))
 
     def __clean_overlapping(self, names):
+        today = date.today().strftime('%Y%m%d')
         names.sort(key=self.__sort_key)
         print(names)
         if len(names) == 1:
@@ -96,7 +97,8 @@         i = 1
         for name in names[1:]:
             this_start, this_end = name.split('_')
             prev_start, prev_end = names[i-1].split('_')
-            if not (this_start < prev_end or this_start == prev_start):
+            if not ((this_start < prev_end and this_start <= today)
+                    or this_start == prev_start):
                 return_names.append(names[i-1])
 
             i = i + 1




diff --git a/converter/server/upload.php b/converter/server/upload.php
index d95574fc8c3975c3a70054c8bab8f5cde9bee938..6b9e7e6b4bbbfcc9483f778ca956d5d2a0b31b81 100644
--- a/converter/server/upload.php
+++ b/converter/server/upload.php
@@ -37,7 +37,13 @@ foreach ($timetables as $id => $timetable) {
     $t = $timetable['t'];
     $sha = $timetable['sha'];
 
-    // todo if $id in $oldMetadata -> skip
+    $shallSkip = false;
+    foreach ($oldMetadata as $entry) {
+        if ($entry['id'] == $id)
+            $shallSkip = true;
+    }
+
+    if ($shallSkip) continue;
 
     $fp = fopen(dirname(__FILE__) . "/$id.db.gz", 'wb');
     $ch = curl_init($t);