Bimba.git

commit 1c7e161e9bf542048dd7eb01e295eda6cfb2c536

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

scheduleless in stops

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


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




diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 40d8d758b7391a5f9ee273336c2185a987c9c309..249294c52d3e85f02f2c65f0a8458e1e26631220 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -44,7 +44,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..81c6bc9779bdbb6cfa7ef4faa87f43af010795a1 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/ProviderProxy.kt b/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt
index 80a47f9442b8b6dec1b40f7acdbf16ad2e34d59e..0dcddcffc601cb437e27a01a15286715735d045b 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt
@@ -1,16 +1,27 @@
 package ml.adamsprogs.bimba
 
-import android.content.Context
+import android.annotation.SuppressLint
+import android.content.*
 import kotlinx.coroutines.experimental.android.UI
 import kotlinx.coroutines.experimental.*
-import ml.adamsprogs.bimba.datasources.VmStopsClient
-import ml.adamsprogs.bimba.models.Timetable
+import ml.adamsprogs.bimba.activities.StopActivity
+import ml.adamsprogs.bimba.datasources.*
+import ml.adamsprogs.bimba.models.*
 import ml.adamsprogs.bimba.models.suggestions.*
+import java.util.*
 
-class ProviderProxy(context: Context) {
-    private val vmStopsClient = VmStopsClient.getVmStopClient()
-    private val timetable: Timetable = Timetable.getTimetable(context)
+//todo make singleton
+class ProviderProxy(context: Context? = null) {
+    private val vmStopsClient = VmClient.getVmStopClient()
+    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) {
@@ -65,6 +76,120 @@                 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: StopActivity, context: StopActivity): 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>): Set<Departure> {
+        if (timetable.isEmpty())
+            return emptySet()
+        else
+            TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
+    }
+
+    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<Int, List<Departure>> {
+        val departures = if (timetable.isEmpty())
+            emptyMap()
+        else
+            timetable.getStopDepartures(stopCode)
+
+        return convertCalendarModes(departures)
+    }
+
+    fun getFullTimetable(stopSegments: Set<StopSegment>): Map<Int, List<Departure>> {
+        val departures = if (timetable.isEmpty())
+            emptyMap()
+        else
+            timetable.getStopDeparturesBySegments(stopSegments)
+
+        return convertCalendarModes(departures)
+    }
+
+    @SuppressLint("UseSparseArrays")
+    private fun convertCalendarModes(raw: Map<String, List<Departure>>): Map<Int, List<Departure>> {
+        val sunday = timetable.getServiceFor(Calendar.SUNDAY)
+        val saturday = timetable.getServiceFor(Calendar.SATURDAY)
+
+        val departures = HashMap<Int, List<Departure>>()
+        departures[StopActivity.MODE_WORKDAYS] =
+                try {
+                    raw.filter { it.key != saturday && it.key != sunday }.toList()[0].second
+                } catch (e: IndexOutOfBoundsException) {
+                    ArrayList<Departure>()
+                }
+
+        departures[StopActivity.MODE_SATURDAYS] = raw[saturday] ?: ArrayList()
+        departures[StopActivity.MODE_SUNDAYS] = raw[sunday] ?: ArrayList()
+
+        return departures
+    }
+
+    interface OnDeparturesReadyListener {
+        fun onDeparturesReady(departures: Set<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>()
+
+        init {
+            receiver.addOnVmListener(this)
+        }
+
+        override fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID, stopCode: String) {
+            if (segments.any { plateId in it }) {
+                if (vmDepartures != null) {
+                    listener.onDeparturesReady(vmDepartures, plateId)
+                    if (plateId !in receivedPlates)
+                        receivedPlates.add(plateId)
+                } else {
+                    receivedPlates.remove(plateId)
+                    if (receivedPlates.isEmpty())
+                        listener.onDeparturesReady(constructSegmentDepartures(segments), plateId)
+                }
+            }
+        }
+
+        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 0c1c1c85ec7488adba5ff3146321fc3397148d18..35d8e4cf583bf4dc49428fff87ec963d663ce135 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
@@ -29,8 +29,8 @@ 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()
     private lateinit var timetable: Timetable
@@ -45,6 +45,7 @@     private val actionModeCallback = ActionModeCallback()
     private var actionMode: ActionMode? = null
     private var isWarned = false
     private lateinit var providerProxy: ProviderProxy
+    private val listenersIds = HashSet<String>()
 
     companion object {
         const val REQUEST_EDIT_FAVOURITE = 1
@@ -224,7 +225,7 @@
     private fun prepareFavourites() {
         favourites = FavouriteStorage.getFavouriteStorage(context)
         favourites.forEach {
-            it.addOnVmPreparedListener(this)
+            listenersIds.add(it.subscribeForDepartures(this, this))
         }
         val layoutManager = LinearLayoutManager(context)
         favouritesList = favourites_list
@@ -234,7 +235,7 @@         favouritesList.itemAnimator = DefaultItemAnimator()
         favouritesList.layoutManager = layoutManager
     }
 
-    override fun onVmPrepared() {
+    override fun onDeparturesReady(departures: Set<Departure>, plateId: Plate.ID) {
         favouritesList.adapter.notifyDataSetChanged()
     }
 
@@ -246,11 +247,10 @@     }
 
     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() {
@@ -258,7 +258,7 @@         if (getDefaultSharedPreferences(this).getBoolean("automatic timetable updates", false))
             startService(Intent(context, TimetableDownloader::class.java))
     }
 
-    override fun onBackPressed() {
+    override fun onBackPressed() { //fixme
         if (drawerLayout.isDrawerOpen(drawerView)) {
             drawerLayout.closeDrawer(drawerView)
             return
@@ -276,8 +276,8 @@     }
 
     override fun onDestroy() {
         super.onDestroy()
+        listenersIds.forEach { providerProxy.unsubscribeFromDepartures(it, this) }
         receiver.removeOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener)
-        favourites.deregisterOnVm(receiver, context)
         unregisterReceiver(receiver)
     }
 




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/StopActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
index 18d6938b4f6cf5b79ed02117238c287f9ec3ecc9..e9e1c623ac6479d893b707dd64b4fc7bf1314e99 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
@@ -15,17 +15,16 @@ 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 {
+class StopActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, ProviderProxy.OnDeparturesReadyListener {
 
     private var sectionsPagerAdapter: SectionsPagerAdapter? = null
 
     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"
@@ -34,50 +33,47 @@
         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 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, Set<Departure>>()
+    private val fullDepartures = HashMap<Int, List<Departure>>()
+    private lateinit var subscriptionId: String
+
 
     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)
+        sectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager, null)
 
         container.adapter = sectionsPagerAdapter
 
@@ -87,51 +83,20 @@
         selectTodayPage()
 
         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 {
+            /* todo
             if (!favourites.has(stopSymbol)) {
                 val items = HashSet<StopSegment>()
                 items.add(stopSegment!!)
@@ -141,6 +106,7 @@             } else {
                 Snackbar.make(it, getString(R.string.stop_already_fav), Snackbar.LENGTH_LONG)
                         .setAction("Action", null).show()
             }
+            */
         }
     }
 
@@ -157,41 +123,40 @@     }
 
     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()
-            }
+    override fun onDeparturesReady(departures: Set<Departure>, plateId: Plate.ID) {
+        this.departures[plateId] = HashSet()
+        (this.departures[plateId]as HashSet).addAll(departures)
+        if (timetableType == TIMETABLE_TYPE_FULL)
             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()
-        }
+        refreshAdapter()
     }
 
-    private fun ticked() = Calendar.getInstance().timeInMillis - lastUpdated >= VmClient.TICK_6_ZINA_TIM_WITH_MARGIN
+    private fun refreshAdapter() {
+        if (timetableType == TIMETABLE_TYPE_FULL)
+            sectionsPagerAdapter!!.departures = fullDepartures
+        else {
+            val departures = HashMap<Int, List<Departure>>()
+            val now = Calendar.getInstance()
+            val tab = now.getMode()
+            val seconds = now.secondsAfterMidnight()
+            departures[tab] = this.departures.flatMap { it.value }.sortedBy { it.timeTill(seconds) }
+            sectionsPagerAdapter!!.departures = departures
+        }
+        sectionsPagerAdapter!!.notifyDataSetChanged()
+    }
 
     override fun onTimetableDownload(result: String?) {
         val message: String = when (result) {
@@ -204,9 +169,7 @@         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()
+        providerProxy.refreshTimetable(this)
     }
 
     private fun selectTodayPage() {
@@ -214,7 +177,8 @@         tabs.getTabAt(sectionsPagerAdapter!!.todayTab())!!.select()
     }
 
     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
     }
 
@@ -222,22 +186,21 @@     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())
+                if (fullDepartures.isEmpty())
+                    if (sourceType == SOURCE_TYPE_STOP)
+                        fullDepartures.putAll(providerProxy.getFullTimetable(stopCode))
+                    else
+                        fullDepartures.putAll(favourite!!.fullTimetable())
+                refreshAdapter()
             } else {
-                timetableType = "departure"
+                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())
+                refreshAdapter()
             }
             return true
         }
@@ -248,18 +211,14 @@
     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(subscriptionId, this)
         unregisterReceiver(receiver)
     }
 
-    inner class SectionsPagerAdapter(fm: FragmentManager, var departures: Map<AgencyAndId, List<Departure>>?) : FragmentStatePagerAdapter(fm) {
+    inner class SectionsPagerAdapter(fm: FragmentManager, var departures: Map<Int, List<Departure>>?) : FragmentStatePagerAdapter(fm) {
         var relativeTime = true
 
         override fun getItem(position: Int): Fragment {
@@ -267,28 +226,7 @@             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")
-            }
+            val list: List<Departure> = departures!![position] ?: ArrayList()
             return PlaceholderFragment.newInstance(list, relativeTime) { updateFabVisibility(it) }
         }
 
@@ -303,7 +241,7 @@             return Calendar.getInstance().getMode()
         }
     }
 
-    class PlaceholderFragment: Fragment() {
+    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)




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 9562d8d1b686e77cf39bd9428497a6a05231801d..a81d3536efc67b4b34a1b4b77337be6ba2379288 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt
@@ -30,7 +30,7 @@         providerProxy.getSheds(name) {
             val layoutManager = LinearLayoutManager(this)
             val departuresList: RecyclerView = list_view
 
-            departuresList.adapter = ShedAdapter(this, it)
+            departuresList.adapter = ShedAdapter(this, it, name)
             departuresList.layoutManager = layoutManager
         }
         /*val timetable = Timetable.getTimetable(this)
@@ -41,7 +41,7 @@         setSupportActionBar(toolbar)
         supportActionBar?.title = name
     }
 
-    class ShedAdapter(val context: Context, private val values: Map<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
@@ -59,11 +59,12 @@                 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_CODE, code)
+                intent.putExtra(StopActivity.EXTRA_STOP_NAME, stopName)
                 context.startActivity(intent)
             }
             holder.stopCode.text = values.keys.sorted()[position]
             holder.stopHeadlines.text = values.entries.sortedBy { it.key }[position].value
-                    .sortedBy { it.split(" → ")[0].toInt() } // fixme<p:1> natural sort
+                    .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..0f6675399ffa2dc686306220cb12ecc95d9b0cc2 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,11 +33,11 @@         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 stopSegment = StopSegment(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),
+                    Plate.ID(it.asJsonObject["line"].asString,
+                            it.asJsonObject["stop"].asString,
                             it.asJsonObject["headsign"].asString)
                 }
                 stopSegment.plates = plates
@@ -90,12 +89,12 @@         for ((name, favourite) in favourites) {
             val timetables = JsonArray()
             for (timetable in favourite.segments) {
                 val segment = JsonObject()
-                segment.addProperty("stop", timetable.stop.id)
+                segment.addProperty("stop", timetable.stop)
                 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("stop", plate.stop)
+                    element.addProperty("line", plate.line)
                     element.addProperty("headsign", plate.headsign)
                     plates.add(element)
                 }
@@ -115,9 +114,9 @@     fun merge(names: List, context: Context) {
         if (names.size < 2)
             return
 
-        val newCache = HashMap<AgencyAndId, ArrayList<Departure>>()
+        val newCache = HashMap<Int, 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 +146,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..88cb8c58bb4adcab4ec092cfd9ecdd508ab369a3 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt
@@ -94,7 +94,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..81820ca45a92a47eb8f3c126748eddcf2721644c 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,101 @@
 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 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 com.google.gson.*
+import kotlinx.coroutines.experimental.*
+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 getVmStopClient(): 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!!))
+        return names.map { StopSuggestion(it, "", "") }
     }
 
-
-    override fun onBind(intent: Intent): IBinder? {
-        return null
-    }
-
-    override fun onDestroy() {
-    }
-
-    @Synchronized
-    private fun downloadVM() {
-        vms.forEach {
-            downloadVM(StopSegment(it.key, it.value.map { it.id }.toSet()))
-        }
-    }
-
-    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)
+    suspend fun makeRequest(method: String, data: String): JsonObject {
         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()
+        println("makeRequest: $request")
+
 
         val responseBody: String?
         try {
-            responseBody = client.newCall(request).execute().body()?.string()
-        } catch (e: IOException) {
-            stopSegment.plates!!.forEach {
-                sendResult(it, null)
-            }
-            return
-        }
-
-        if (responseBody?.get(0) == '<') {
-            stopSegment.plates!!.forEach {
-                sendResult(it, null)
+            responseBody = withContext(CommonPool) {
+                client.newCall(request).execute().body()?.string()
             }
-            return
+        } catch (e: IOException) {
+            return 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)
-            }
+        return try {
+            Gson().fromJson(responseBody, JsonObject::class.java)
+        } catch (e: JsonSyntaxException) {
+            JsonObject()
         }
-
-        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)
     }
-
-    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)
-    }
-
-    data class Request(val plate: Plate.ID, var times: Int)
-
-    class EmptyStopSegmentException : Exception()
 }
-
-//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..d9829c86ccecc42d4ab362ade5e6e9a36be5511b
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmService.kt
@@ -0,0 +1,187 @@
+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 {
+        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(this)) {
+            vms[stopCode] = emptySet()
+            sendResult(stopCode, null, null)
+            return
+        }
+
+        val javaRootMapObject = VmClient.getVmStopClient().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/datasources/VmStopsClient.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmStopsClient.kt
deleted file mode 100644
index 7f9d5e8562a5c1f064dbc9c7fb6c4f9b422cdf53..0000000000000000000000000000000000000000
--- a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmStopsClient.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-package ml.adamsprogs.bimba.datasources
-
-import com.google.gson.*
-import kotlinx.coroutines.experimental.*
-import ml.adamsprogs.bimba.models.Plate
-import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
-import ml.adamsprogs.bimba.models.suggestions.*
-import okhttp3.*
-import java.io.IOException
-import java.util.*
-import kotlin.collections.HashMap
-import kotlin.collections.HashSet
-
-class VmStopsClient {
-    companion object {
-        private var vmStopsClient: VmStopsClient? = null
-
-        fun getVmStopClient(): VmStopsClient {
-            if (vmStopsClient == null)
-                vmStopsClient = VmStopsClient()
-            return vmStopsClient!!
-        }
-    }
-
-    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 result
-    }
-
-    /*
-    suspend fun getPlatesByStopPoint(code: String): Set<Plate.ID>? {
-        val getTimesResponse = makeRequest("getTimes", """{"symbol": "$code"}""")
-        val name = getTimesResponse["success"].asJsonObject["bollard"].asJsonObject["name"].asString
-
-        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"}""")
-        }
-
-        if (!response.has("success"))
-            return emptyList()
-
-        val points = response["success"].asJsonArray.map { it.asJsonObject }
-
-        val names = HashSet<String>()
-
-        points.forEach {
-            val name = it["name"].asString
-            names.add(name)
-        }
-
-        return names.map { StopSuggestion(it, "", "") }
-    }
-
-    private suspend fun makeRequest(method: String, data: String): JsonObject {
-        val client = OkHttpClient()
-        val url = "http://www.peka.poznan.pl/vm/method.vm?ts=${Calendar.getInstance().timeInMillis}"
-        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(body)
-                .build()
-        println("makeRequest: $request")
-
-
-        val responseBody: String?
-        try {
-            responseBody = withContext(CommonPool) {
-                client.newCall(request).execute().body()?.string()
-            }
-        } catch (e: IOException) {
-            return JsonObject()
-        }
-
-
-        return Gson().fromJson(responseBody, JsonObject::class.java)
-    }
-}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
index cc04631c1708de25c1758299bc19ba0978599b2c..e398189fe5652b37dd473d40edf9927cf620e31a 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt
@@ -80,7 +80,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)




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..35aeba4e97ff3df8a71f5742d7b49f2fb8a2bb8e 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,15 +41,15 @@             }
             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>>()
+        fun rollDepartures(departures: Map<Int, List<Departure>>): Map<Int, 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<Int, List<Departure>>()
             departures.keys.forEach {
                 val (filtered, isFull) = filterDepartures(departures[it]!!)
                 if (isFull as Boolean) {
@@ -80,9 +79,9 @@         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 +95,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..87df063adc773a421f92d0c37b6d34c110c8af90 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,27 @@
 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<Int, List<Departure>> = HashMap()
+    private var cache: Set<Departure> = HashSet()
 
     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 +39,29 @@         val mapDir = File(parcel.readString())
 
         val mapString = mapDir.readText()
 
-        val map = HashMap<AgencyAndId, List<Departure>>()
-        mapString.safeSplit("%").forEach {
+        val map = HashMap<Int, List<Departure>>()
+        mapString.safeSplit("%")!!.forEach { it ->
             val (k, v) = it.split("#")
-            map[AgencyAndId(k)] = v.split("&").map { Departure.fromString(it) }
+            map[k.toInt()] = 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<Int, 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 +82,7 @@         dest?.writeString(mapFile.absolutePath)
 
         var isFirst = true
         var map = ""
-        fullDepartures.forEach {
+        fullDepartures.forEach { it ->
             if (isFirst)
                 isFirst = false
             else
@@ -105,15 +93,6 @@         }
         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) {
         segments.forEach {
             it.remove(plateId)
@@ -121,35 +100,6 @@         }
         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)
-            }
-        }
-    }
-
     fun rename(newName: String) {
         name = newName
     }
@@ -164,80 +114,39 @@             return arrayOfNulls(size)
         }
     }
 
-    fun nextDeparture(): Departure? {
-        val now = Calendar.getInstance().secondsAfterMidnight()
-        filterVmDepartures()
-        if (segments.isEmpty() && vmDepartures.isEmpty())
-            return null
+    fun nextDeparture() =
+            if (cache.isEmpty())
+                null
+            else
+                cache.sortedBy { it.time }[0]
 
-        if (vmDepartures.isNotEmpty()) {
-            return vmDepartures.flatMap { it.value }
-                    .minBy {
-                        it.timeTill(now)
-                    }
-        }
 
-        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<Int, 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<Int, 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
+        return providerProxy.subscribeForDepartures(segments, this, context)
     }
 
-    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: Set<Departure>, plateId: Plate.ID) {
+        cache = departures
+        listener.onDeparturesReady(departures, plateId)
     }
 
-    interface OnVmPreparedListener {
-        fun onVmPrepared()
+    fun unsubscribeFromDepartures(uuid: String, context: Context) {
+        providerProxy.unsubscribeFromDepartures(uuid, 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..8fb050a63053bf54097da8d1048f746fe74bae98 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,15 @@             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 {
             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..07148cbe06fa0bc5f3d6074db888601dc8f0d469 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> {
@@ -19,10 +18,6 @@
         override fun newArray(size: Int): Array<StopSegment?> {
             return arrayOfNulls(size)
         }
-    }
-
-    fun fillPlates() {
-        plates = Timetable.getTimetable().getPlatesForStop(stop)
     }
 
     override fun writeToParcel(dest: Parcel?, flags: Int) {
@@ -30,7 +25,7 @@         dest?.writeSerializable(stop)
         if (plates != null)
             dest?.writeString(plates!!.joinToString(";") { it.toString() })
         else
-            dest?.writeString("")
+            dest?.writeString("null")
     }
 
     override fun describeContents(): Int {
@@ -56,12 +51,14 @@         return false
     }
 
     override fun hashCode(): Int {
-        return super.hashCode()
+        var hashCode = stop.hashCode()
+        plates?.forEach { hashCode = 31 * hashCode + it.hashCode() }
+        return hashCode
     }
 
-    fun contains(plateId: Plate.ID): Boolean {
+    operator fun contains(plateId: Plate.ID): Boolean {
         if (plates == null)
-            return false
+            return plateId.stop == stop
         return plates!!.contains(plateId)
     }
 




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 f09c969c9554054ce02e4ffd3b653422e795d39a..f00bd5680b147a5006f477d30922a8ba5b6df40a 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
@@ -3,6 +3,7 @@
 import android.annotation.SuppressLint
 import android.content.Context
 import android.database.*
+import android.database.sqlite.SQLiteCantOpenDatabaseException
 import android.database.sqlite.SQLiteDatabase
 import android.database.sqlite.SQLiteException
 import ml.adamsprogs.bimba.*
@@ -139,9 +140,9 @@         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 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()
@@ -149,9 +150,9 @@
         return name
     }
 
-    fun getStopCode(stopId: AgencyAndId): String {
+    fun getStopCode(stopId: String): String {
         val cursor = db!!.rawQuery("select stop_code from stops where stop_id = ?",
-                arrayOf(stopId.id))
+                arrayOf(stopId))
         cursor.moveToNext()
         val code = cursor.getString(0)
         cursor.close()
@@ -159,16 +160,16 @@
         return code
     }
 
-    fun getStopDepartures(stopId: AgencyAndId): Map<AgencyAndId, List<Departure>> {
-        val map = HashMap<AgencyAndId, ArrayList<Departure>>()
+    fun getStopDepartures(stopCode: String): Map<String, List<Departure>> {
+        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))
+                "from stop_times natural join trips natural join routes where stop_code = ?",
+                arrayOf(stopCode))
 
         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
@@ -192,10 +193,10 @@
         return map
     }
 
-    fun getStopDeparturesBySegments(segments: HashSet<StopSegment>): Map<AgencyAndId, List<Departure>> {
+    fun getStopDeparturesBySegments(segments: Set<StopSegment>): Map<String, List<Departure>> {
         val wheres = segments.flatMap {
             it.plates?.map {
-                "(stop_id = ${it.stop} and route_id = '${it.line}' and trip_headsign = '${it.headsign}')"
+                "(stop_code = ${it.stop} and route_id = '${it.line}' and trip_headsign = '${it.headsign}')"
             } ?: listOf()
         }.joinToString(" or ")
 
@@ -208,12 +209,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
@@ -245,10 +246,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))
+                arrayOf(serviceId))
 
         cursor.moveToNext()
         (1 until 7).forEach {
@@ -264,9 +265,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]!!)
             }
         }
 
@@ -297,15 +298,15 @@             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")
@@ -345,19 +346,19 @@         cursor.close()
         return validTill
     }
 
-    fun getServiceForToday(): AgencyAndId {
+    fun getServiceForToday(): String {
         val today = JCalendar.getInstance().get(JCalendar.DAY_OF_WEEK)
         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)
     }
 
-    fun getServiceFor(day: Int): AgencyAndId {
+    fun getServiceFor(day: Int): String {
         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)
 
@@ -366,20 +367,20 @@         cursor.moveToNext()
         try {
             service = cursor.getInt(0)
             cursor.close()
-            return AgencyAndId(service.toString())
+            return service.toString()
         } catch (e: CursorIndexOutOfBoundsException) {
             throw IllegalArgumentException()
         }
     }
 
-    fun getPlatesForStop(stop: AgencyAndId): Set<Plate.ID> {
+    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))
+                "from stop_times natural join trips 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))
         }
@@ -388,13 +389,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, " +
                 "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)




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/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 af9fc5a346c5ef4d8dea9e54a4ef0e3a635cc836..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,7 +3,6 @@
 import android.os.Parcel
 import android.os.Parcelable
 import ml.adamsprogs.bimba.R
-import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
 
 class StopSuggestion(name: String, private val zone: String, private val zoneColour: String) : GtfsSuggestion(name){
     @Suppress("UNCHECKED_CAST")




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