Bimba.git

commit 16334df917724754c33c76afd31f0d89b6f7a185

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

Merge branch 'v2.0'

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


diff --git a/app/build.gradle b/app/build.gradle
index ceec2719de9cdc6a9df162cb04300385ca2912b3..60d7ff4dd1cc7f634f2ea63bade2a44142788dd3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -4,13 +4,13 @@ apply plugin: 'kotlin-android-extensions'
 
 android {
     compileSdkVersion 27
-    buildToolsVersion "28.0.1"
+    buildToolsVersion "28.0.2"
     defaultConfig {
         applicationId "ml.adamsprogs.bimba"
         minSdkVersion 19
         targetSdkVersion 27
-        versionCode 14
-        versionName "2.0-beta4"
+        versionCode 15
+        versionName "2.0"
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
         vectorDrawables.useSupportLibrary = true
     }
@@ -31,7 +31,7 @@     implementation 'com.android.support:appcompat-v7:27.1.1'
     implementation 'com.android.support:cardview-v7:27.1.1'
     implementation 'com.android.support:design:27.1.1'
     implementation 'com.android.support:support-vector-drawable:27.1.1'
-    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
+    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
     implementation 'com.github.arimorty:floatingsearchview:2.1.1'
     implementation 'com.google.code.gson:gson:2.8.1'




diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5f4681fca49b83a45a0610ee2e176da39900d4ec..e0b120550519b6a66af16dd4c18b7fe0c5606fcc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,6 +9,7 @@
     <application
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
+        android:roundIcon="@mipmap/ic_launcher_round"
         android:label="@string/app_name"
         android:supportsRtl="true"
         android:theme="@style/AppTheme">
@@ -27,7 +28,6 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-        <activity android:name=".activities.NoDbActivity" />
         <activity android:name=".activities.EditFavouriteActivity" />
         <activity
             android:name=".activities.SettingsActivity"
@@ -44,7 +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..faedcc757de71be595d9cd42158d306e3d44964d 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,13 @@             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
+            val code = intent.getIntExtra(VmService.EXTRA_CODE, 0)
             for (listener in onVmListeners) {
-                listener.onVm(departures, plateId)
+                listener.onVm(departures, plateId, stopCode, code)
             }
         }
     }
@@ -58,6 +60,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, code: Int)
     }
 }
\ No newline at end of file




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




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




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




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt
index 372f6448099ab199590f3e2e2241039aff3c3af1..d5c812f0e1dccc101297f852d23cd0756d39f300 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt
@@ -42,7 +42,7 @@         val layoutManager = LinearLayoutManager(this)
         recyclerView!!.layoutManager = layoutManager
         val dividerItemDecoration = DividerItemDecoration(this, layoutManager.orientation)
         recyclerView.addItemDecoration(dividerItemDecoration)
-        recyclerView.adapter = FavouriteEditRowAdapter(favourite!!)
+        recyclerView.adapter = FavouriteEditRowAdapter(favourite!!, favourite_edit_loading, favourite_edit_list)
         setSupportActionBar(toolbar)
         supportActionBar?.title = getString(R.string.edit_favourite_title, favourite!!.name)
         nameEdit = favourite_name_edit




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




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




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




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




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




diff --git a/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt b/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt
index 2c8e92efd8f69559ae0ec5cc2eadda253d49c80f..b7d96b89b3fd738796ade8db57b8539e794ba59e 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
 
 
@@ -30,21 +29,23 @@     private val preferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE)
 
     init {
         val favouritesString = preferences.getString("favourites", "{}")
-        val favouritesMap = Gson().fromJson(favouritesString, JsonObject::class.java)
-        for ((name, jsonTimetables) in favouritesMap.entrySet()) {
-            val timetables = HashSet<StopSegment>()
-            jsonTimetables.asJsonArray.mapTo(timetables) {
-                val stopSegment = StopSegment(AgencyAndId(it.asJsonObject["stop"].asString), null)
-                val plates = HashSet<Plate.ID>()
-                it.asJsonObject["plates"].asJsonArray.mapTo(plates) {
-                    Plate.ID(AgencyAndId(it.asJsonObject["line"].asString),
-                            AgencyAndId(it.asJsonObject["stop"].asString),
-                            it.asJsonObject["headsign"].asString)
+        JsonParser().parse(favouritesString).asJsonObject.entrySet().forEach { (name, timetables) ->
+            timetables.asJsonArray.map {
+                val plates = it.asJsonObject["plates"].let {
+                    if (it == null || it.isJsonNull)
+                        null
+                    else {
+                        it.asJsonArray.map {
+                            it.asJsonObject.let {
+                                Plate.ID(it["line"].asString, it["stop"].asString, it["headsign"].asString)
+                            }
+                        }.toHashSet()
+                    }
                 }
-                stopSegment.plates = plates
-                stopSegment
+                StopSegment(it.asJsonObject["stop"].asString, plates)
+            }.toHashSet().let {
+                favourites[name] = Favourite(name, it, context)
             }
-            favourites[name] = Favourite(name, timetables, context)
             positionIndex.add(name)
         }
     }
@@ -79,9 +80,11 @@         positionIndex.remove(name)
         serialize()
     }
 
-    fun delete(name: String, plate: Plate.ID) {
-        favourites[name]?.delete(plate)
-        serialize()
+    fun delete(name: String, plate: Plate.ID): Boolean {
+        return favourites[name]?.delete(plate).let {
+            serialize()
+            it
+        } ?: false
     }
 
     private fun serialize() {
@@ -90,15 +93,20 @@         for ((name, favourite) in favourites) {
             val timetables = JsonArray()
             for (timetable in favourite.segments) {
                 val segment = JsonObject()
-                segment.addProperty("stop", timetable.stop.id)
-                val plates = JsonArray()
-                for (plate in timetable.plates ?: HashSet()) {
-                    val element = JsonObject()
-                    element.addProperty("stop", plate.stop.id)
-                    element.addProperty("line", plate.line.id)
-                    element.addProperty("headsign", plate.headsign)
-                    plates.add(element)
-                }
+                segment.addProperty("stop", timetable.stop)
+                val plates =
+                        if (timetable.plates == null)
+                            JsonNull.INSTANCE
+                        else
+                            JsonArray().apply {
+                                for (plate in timetable.plates ?: HashSet()) {
+                                    val element = JsonObject()
+                                    element.addProperty("stop", plate.stop)
+                                    element.addProperty("line", plate.line)
+                                    element.addProperty("headsign", plate.headsign)
+                                    add(element)
+                                }
+                            }
                 segment.add("plates", plates)
                 timetables.add(segment)
             }
@@ -108,16 +116,15 @@         val favouritesString = Gson().toJson(rootObject)
         val editor = preferences.edit()
         editor.putString("favourites", favouritesString)
         editor.apply()
-
     }
 
     fun merge(names: List<String>, context: Context) {
         if (names.size < 2)
             return
 
-        val newCache = HashMap<AgencyAndId, ArrayList<Departure>>()
+        val newCache = HashMap<String, ArrayList<Departure>>()
         names.forEach {
-            favourites[it]!!.fullDepartures.forEach {
+            favourites[it]!!.fullTimetable().forEach {
                 if (newCache[it.key] == null)
                     newCache[it.key] = ArrayList()
                 newCache[it.key]!!.addAll(it.value)
@@ -149,24 +156,17 @@         addIndex(newName)
         serialize()
     }
 
-    fun registerOnVm(receiver: MessageReceiver, context: Context) {
-        favourites.values.forEach {
-            it.registerOnVm(receiver, context)
-        }
-    }
-
-    fun deregisterOnVm(receiver: MessageReceiver, context: Context) {
-        favourites.values.forEach {
-            it.deregisterOnVm(receiver, context)
-        }
-    }
-
     operator fun get(name: String): Favourite? {
         return favourites[name]
     }
 
     operator fun get(position: Int): Favourite? {
         return favourites[positionIndex[position]]
+    }
+
+    operator fun set(name: String, value: Favourite) {
+        favourites[name] = value
+        serialize()
     }
 
     fun indexOf(name: String): Int {




diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/CacheManager.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/CacheManager.kt
deleted file mode 100644
index 71130764f796ebe17e68897bd6ec9899accfe5b1..0000000000000000000000000000000000000000
--- a/app/src/main/java/ml/adamsprogs/bimba/datasources/CacheManager.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-package ml.adamsprogs.bimba.datasources
-
-import android.content.Context
-import android.content.SharedPreferences
-import ml.adamsprogs.bimba.models.Plate
-import ml.adamsprogs.bimba.models.gtfs.AgencyAndId
-
-class CacheManager private constructor(context: Context) {
-    companion object {
-        private var manager: CacheManager? = null
-        fun getCacheManager(context: Context): CacheManager {
-            return if (manager == null) {
-                manager = CacheManager(context)
-                manager!!
-            } else
-                manager!!
-        }
-
-        val MAX_SIZE = 40
-    }
-
-    private var cachePreferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.cachePreferences.cache", Context.MODE_PRIVATE)
-    private var cacheHitsPreferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.cachePreferences.cacheHits", Context.MODE_PRIVATE)
-
-    private var cache: HashMap<String, Plate> = HashMap()
-    private var cacheHits: HashMap<String, Int> = HashMap()
-
-    fun keys(): List<Plate> {
-        return cache.map {
-            Plate(Plate.ID(
-                    AgencyAndId.convertFromString(it.key.split("@")[0]),
-                    AgencyAndId.convertFromString(it.key.split("@")[1].split(">")[0]),
-                    it.key.split(">")[1]),
-                    null)
-        }
-    }
-
-    fun hasAll(plates: HashSet<Plate>): Boolean {
-        plates
-                .filterNot { has(it) }
-                .forEach { return false }
-        return true
-    }
-
-    fun hasAny(plates: HashSet<Plate>): Boolean {
-        plates
-                .filter { has(it) }
-                .forEach { return true }
-        return false
-    }
-
-    fun has(plate: Plate): Boolean {
-        return cache.containsKey(key(plate))
-    }
-
-    fun push(plates: HashSet<Plate>) {
-        val removeNumber = cache.size + plates.size - MAX_SIZE
-        val editor = cachePreferences.edit()
-        val editorCacheHits = cacheHitsPreferences.edit()
-        cacheHits.map { "${it.value}|${it.key}" }.sortedBy { it }.slice(0 until removeNumber).forEach {
-            val key = it.split("|")[1]
-            cache.remove(key)
-            editor.remove(key)
-        }
-        for (plate in plates) {
-            val key = key(plate)
-            cache[key] = plate
-            cacheHits[key] = 0
-            editor.putString(key, cache[key].toString())
-            editorCacheHits.putInt(key, 0)
-        }
-        editor.apply()
-        editorCacheHits.apply()
-    }
-
-    fun push(plate: Plate) {
-        val editorCache = cachePreferences.edit()
-        val editorCacheHits = cacheHitsPreferences.edit()
-        if (cacheHits.size == MAX_SIZE) {
-            val key = cacheHits.minBy { it.value }?.key
-            cache.remove(key)
-            editorCache.remove(key)
-            cacheHits.remove(key)
-            editorCacheHits.remove(key)
-        }
-        val key = key(plate)
-        cache[key] = plate
-        cacheHits[key] = 0
-        editorCache.putString(key, plate.toString())
-        editorCacheHits.putInt(key, 0)
-        editorCache.apply()
-        editorCacheHits.apply()
-    }
-
-    fun get(plates: HashSet<Plate>): HashSet<Plate> {
-        val result = HashSet<Plate>()
-        for (plate in plates) {
-            val value = get(plate)
-            if (value == null)
-                result.add(plate)
-            else
-                result.add(value)
-        }
-        return result
-    }
-
-    fun get(plate: Plate): Plate? {
-        if (!has(plate))
-            return null
-        val key = key(plate)
-        val hits = cacheHits[key]
-        if (hits != null)
-            cacheHits[key] = hits + 1
-        return cache[key]
-    }
-
-    fun recreate(stopDeparturesByPlates: Set<Plate>) {
-        stopDeparturesByPlates.forEach { cache[key(it)] = it }
-    }
-
-    init {
-        cache = cacheFromString(cachePreferences.all)
-        @Suppress("UNCHECKED_CAST")
-        cacheHits = cacheHitsPreferences.all as HashMap<String, Int>
-    }
-
-    private fun cacheFromString(preferences: Map<String, *>): HashMap<String, Plate> {
-        val result = HashMap<String, Plate>()
-        for ((key, value) in preferences.entries) {
-            result[key] = Plate.fromString(value as String)
-        }
-        return result
-    }
-
-    private fun key(plate: Plate) = "${plate.id.line}@${plate.id.stop}>${plate.id.headsign}"
-}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt
index 7fcb3e1ac4c36f0e0bcafc7fcc995bb468e920c7..8c39cb5c95f88327c5aa654d3d4b84e701fdcad6 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt
@@ -31,7 +31,7 @@
         if (intent != null) {
             notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
             val prefs = this.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE)!!
-            if (!NetworkStateReceiver.isNetworkAvailable(this)) {
+            if (!NetworkStateReceiver.isNetworkAvailable()) {
                 sendResult(RESULT_NO_CONNECTIVITY)
                 return
             }
@@ -57,7 +57,6 @@                     sendResult(RESULT_UP_TO_DATE)
                     return
                 }
                 if (httpCon.responseCode != HttpsURLConnection.HTTP_OK) {
-                    println(httpCon.responseMessage)
                     sendResult(RESULT_NO_CONNECTIVITY)
                     return
                 }
@@ -94,7 +93,7 @@             prefsEditor.putString("etag", newETag)
             prefsEditor.apply()
 
             val oldDb = File(getSecondaryExternalFilesDir(), "timetable.db")
-            gtfsDb.renameTo(oldDb) // todo<p:1> delete old before downloading (may require stopping VmClient), and mutex with VmClient
+            gtfsDb.renameTo(oldDb) // todo<p:1> delete old before downloading (may require stopping VmService), and mutex with VmService
 
             cancelNotification()
 




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




diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmService.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..82e4f38941d51829f3e42771115968d48869b25a
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmService.kt
@@ -0,0 +1,192 @@
+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 EXTRA_CODE = "ml.adamsprogs.bimba.extra.vm.code"
+        const val TICK_6_ZINA_TIM = 12500L
+        const val TICK_6_ZINA_TIM_WITH_MARGIN = TICK_6_ZINA_TIM * 3 / 4
+    }
+
+    private var handler: Handler? = null
+    private val tick6ZinaTim: Runnable = object : Runnable {
+        override fun run() {
+            handler!!.postDelayed(this, TICK_6_ZINA_TIM)
+            try {
+                for (plateId in requests.keys)
+                    launch(UI) {
+                        withContext(DefaultDispatcher) {
+                            downloadVM()
+                        }
+                    }
+            } catch (e: IllegalArgumentException) {
+            }
+        }
+    }
+    private val requests = HashMap<String, Int>()
+    private val vms = HashMap<String, Set<Plate>>()
+
+    override fun onCreate() {
+        val thread = HandlerThread("ServiceStartArguments", THREAD_PRIORITY_BACKGROUND)
+        thread.start()
+        handler = Handler(thread.looper)
+        handler!!.postDelayed(tick6ZinaTim, TICK_6_ZINA_TIM)
+    }
+
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        if (intent == null)
+            return START_STICKY
+        val stopCode = intent.getStringExtra("stop")!!
+        val action = intent.action
+        val once = intent.getBooleanExtra("once", false)
+        if (action == "request") {
+            if (isAlreadyRequested(stopCode)) {
+                incrementRequest(stopCode)
+                sendResult(stopCode)
+            } else {
+                if (!once)
+                    addRequest(stopCode)
+                launch(UI) {
+                    withContext(DefaultDispatcher) {
+                        downloadVM(stopCode)
+                    }
+                }
+            }
+        } else if (action == "remove") {
+            decrementRequest(stopCode)
+            cleanRequests()
+        }
+        return START_STICKY
+    }
+
+    private fun cleanRequests() {
+        val newRequests = requests.filter { it.value > 0 }
+        requests.clear()
+        newRequests.forEach {
+            requests[it.key] = it.value
+        }
+    }
+
+    private fun addRequest(stopCode: String) {
+        if (requests[stopCode] == null)
+            requests[stopCode] = 0
+        requests[stopCode] = requests[stopCode]!! + 1
+    }
+
+    private fun incrementRequest(stopCode: String) {
+        requests[stopCode] = requests[stopCode]!! + 1
+    }
+
+    private fun decrementRequest(stopCode: String) {
+        requests[stopCode] = requests[stopCode]!! - 1
+    }
+
+    private fun isAlreadyRequested(stopCode: String): Boolean {
+        return stopCode in requests
+    }
+
+
+    override fun onBind(intent: Intent): IBinder? {
+        return null
+    }
+
+    override fun onDestroy() {
+    }
+
+    private suspend fun downloadVM() {
+        vms.forEach {
+            downloadVM(it.key)
+        }
+    }
+
+    private suspend fun downloadVM(stopCode: String) {
+        if (!NetworkStateReceiver.isNetworkAvailable()) {
+            vms[stopCode] = emptySet()
+            sendResult(stopCode, null, null, 0)
+            return
+        }
+
+        val (code, javaRootMapObject) = VmClient.getVmClient().makeRequest("getTimes", """{"symbol": "$stopCode"}""")
+
+        if (!javaRootMapObject.has("success")) {
+            sendResult(stopCode, null, null, code)
+            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>?, code: Int = 200) {
+        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_CODE, code)
+        broadcastIntent.putExtra(EXTRA_PLATE_ID, plateId)
+        broadcastIntent.putExtra(EXTRA_STOP_CODE, stopCode)
+        sendBroadcast(broadcastIntent)
+    }
+}
+
+//note application stops the service on exit
+




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




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




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




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




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt b/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt
index 5197e6e17237a3b8ab3e28880b875321ddb6fda3..e6ef8860315c2d2aca5a481267fb924954bf9ceb 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,17 +51,35 @@         return false
     }
 
     override fun hashCode(): Int {
-        return super.hashCode()
+        var hashCode = stop.hashCode()
+        plates?.forEach { hashCode = 31 * hashCode + it.hashCode() }
+        return hashCode
+    }
+
+    operator fun contains(plateId: Plate.ID): Boolean {
+        if (plates == null)
+            return plateId.stop == stop
+        return plates!!.contains(plateId)
     }
 
-    fun contains(plateId: Plate.ID): Boolean {
+    fun remove(plateId: Plate.ID): Boolean {
         if (plates == null)
             return false
-        return plates!!.contains(plateId)
+
+        plates = plates!!.asSequence().filter { it != plateId }.toSet()
+        return true
     }
 
-    fun remove(plateId: Plate.ID) {
-        (plates as HashSet).remove(plateId)
+    override fun toString(): String {
+        var s = "$stop: "
+        if (plates == null)
+            s += "NULL"
+        else {
+            s += "{"
+            s += plates!!.joinToString { it.toString() }
+            s += "}"
+        }
+        return s
     }
 
     val size: Int




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




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt
index 13a2f5765c5ec92fd13aa9ec8f05681c5f3ce0ed..920b6741fea569dcdc950c55aa77a78cf110732e 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt
@@ -16,41 +16,44 @@ import ml.adamsprogs.bimba.models.Departure
 import ml.adamsprogs.bimba.rollTime
 import java.util.*
 
-class DeparturesAdapter(val context: Context, private val departures: List<Departure>?, private val relativeTime: Boolean) :
+class DeparturesAdapter(val context: Context, var departures: List<Departure>?, var relativeTime: Boolean) :
         RecyclerView.Adapter<DeparturesAdapter.ViewHolder>() {
 
     companion object {
         const val VIEW_TYPE_LOADING: Int = 0
         const val VIEW_TYPE_CONTENT: Int = 1
-        const val VIEW_TYPE_EMPTY: Int = 2
     }
 
     override fun getItemCount(): Int {
-        if (departures == null || departures.isEmpty())
+        if (departures == null || departures!!.isEmpty())
             return 1
-        return departures.size
+        return departures!!.size
     }
 
     override fun getItemViewType(position: Int): Int {
         return when {
-            departures == null -> VIEW_TYPE_EMPTY
-            departures.isEmpty() -> VIEW_TYPE_LOADING
+            departures == null -> VIEW_TYPE_LOADING //empty
+            departures!!.isEmpty() -> VIEW_TYPE_LOADING
             else -> VIEW_TYPE_CONTENT
         }
     }
 
     override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        holder.floorIcon.visibility = View.GONE
+        holder.infoIcon.visibility = View.GONE
+
         if (departures == null) {
             return
         }
+
         val line = holder.lineTextView
         val time = holder.timeTextView
         val direction = holder.directionTextView
-        if (departures.isEmpty()) {
+        if (departures!!.isEmpty()) {
             time.text = context.getString(R.string.no_departures)
             return
         }
-        val departure = departures[position]
+        val departure = departures!![position]
         val now = Calendar.getInstance()
         val departureTime = Calendar.getInstance().rollTime(departure.time)
         if (departure.tomorrow)
@@ -77,9 +80,8 @@             icon.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_departure_timetable, context.theme))
 
         if (departure.lowFloor)
             holder.floorIcon.visibility = View.VISIBLE
-        if (departure.isModified) {
+        if (departure.isModified)
             holder.infoIcon.visibility = View.VISIBLE
-        }
         holder.root.setOnClickListener {
             AlertDialog.Builder(context)
                     .setPositiveButton(context.getText(android.R.string.ok)




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt
index 6814ada2b4f7f4e9ff219337c1a4e1e03c643fae..cb78df9d44a97addec4f60b2c1539551311584aa 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt
@@ -6,29 +6,77 @@ import android.view.View
 import android.view.ViewGroup
 import android.widget.ImageView
 import android.widget.TextView
+import kotlinx.coroutines.experimental.DefaultDispatcher
+import kotlinx.coroutines.experimental.android.UI
+import kotlinx.coroutines.experimental.launch
+import kotlinx.coroutines.experimental.withContext
+import ml.adamsprogs.bimba.ProviderProxy
 import ml.adamsprogs.bimba.R
 import ml.adamsprogs.bimba.collections.FavouriteStorage
 import ml.adamsprogs.bimba.models.Favourite
 import ml.adamsprogs.bimba.models.Plate
-import ml.adamsprogs.bimba.models.Timetable
+import ml.adamsprogs.bimba.models.StopSegment
+
 
-class FavouriteEditRowAdapter(private var favourite: Favourite) :
+class FavouriteEditRowAdapter(private var favourite: Favourite, private val loadingView: View, private val listView: View) :
         RecyclerView.Adapter<FavouriteEditRowAdapter.ViewHolder>() {
-    override fun getItemCount(): Int {
-        return favourite.size
+
+    private val segments = HashMap<String, StopSegment>()
+    private val providerProxy = ProviderProxy()
+    private val favourites = FavouriteStorage.getFavouriteStorage()
+    private val platesList = ArrayList<Plate.ID>()
+    private val namesList = HashMap<Plate.ID, String>()
+
+    init {
+        launch(UI) {
+            withContext(DefaultDispatcher) {
+                favourite.segments.forEach {
+                    if (it.plates == null) {
+                        (providerProxy.fillStopSegment(it) ?: it).let { segment ->
+                            segments[segment.stop] = segment
+                            it.plates = segment.plates
+                        }
+                    } else {
+                        segments[it.stop] = it
+                    }
+                }
+                favourites[favourite.name] = favourite
+
+                segments.flatMap {
+                    it.value.plates ?: emptyList<Plate.ID>()
+                }.sortedBy { "${it.line}${it.stop}" }.forEach {
+                    platesList.add(it)
+                    namesList[it] = providerProxy.getStopName(it.stop).let { name ->
+                        "${name ?: ""} (${it.stop}):\n${it.line} → ${it.headsign}"
+                    }
+                }
+                launch(UI) {
+                    loadingView.visibility = View.GONE
+                    listView.visibility = View.VISIBLE
+                    this@FavouriteEditRowAdapter.notifyDataSetChanged()
+                }
+            }
+        }
     }
 
+
+    override fun getItemCount(): Int = platesList.size
+
     override fun onBindViewHolder(holder: ViewHolder, position: Int) {
-        val timetable = Timetable.getTimetable()
-        val favourites = FavouriteStorage.getFavouriteStorage()
-        val id = favourite.segments.flatMap { it.plates!! }.sortedBy { "${it.line}${it.stop}"}[position]
-        val plate = Plate(id, null)
-        val favouriteElement = "${timetable.getStopName(plate.id.stop)} ( ${timetable.getStopCode(plate.id.stop)}):\n${plate.id.line} → ${plate.id.headsign}"
-        holder.rowTextView.text = favouriteElement
-        holder.deleteButton.setOnClickListener {
-            favourites.delete(favourite.name, id)
-            favourite = favourites.favourites[favourite.name]!!
-            notifyDataSetChanged()
+        launch(UI) {
+            val id = platesList[position]
+            val favouriteElement = namesList[id]
+
+            holder.rowTextView.text = favouriteElement
+            holder.deleteButton.setOnClickListener {
+                launch(UI) {
+                    favourites.delete(favourite.name, id)
+                    favourite = favourites.favourites[favourite.name]!!
+                    notifyItemRemoved(platesList.indexOf(id))
+                    platesList.remove(id)
+                    namesList.remove(id)
+                }
+            }
         }
     }
 
@@ -41,7 +89,7 @@         return ViewHolder(rowView)
     }
 
     inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
-        val rowTextView:TextView = itemView.findViewById(R.id.favourite_edit_row)
-        val deleteButton:ImageView = itemView.findViewById(R.id.favourite_edit_delete)
+        val rowTextView: TextView = itemView.findViewById(R.id.favourite_edit_row)
+        val deleteButton: ImageView = itemView.findViewById(R.id.favourite_edit_delete)
     }
 }
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt
index 6cef0867376e4bcf1b8aecf4df91fc3b683ede7f..a30130f8c29ad35d2a33a8cefa0ef22786ca5282 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,17 +9,16 @@ 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
+import ml.adamsprogs.bimba.models.Favourite
 import ml.adamsprogs.bimba.secondsAfterMidnight
 
 
-class FavouritesAdapter(val appContext: Context, var favourites: FavouriteStorage,
+class FavouritesAdapter(private val appContext: Context, var favourites: FavouriteStorage,
                         private val onMenuItemClickListener: OnMenuItemClickListener,
                         private val onClickListener: ViewHolder.OnClickListener) :
         RecyclerView.Adapter<FavouritesAdapter.ViewHolder>() {
@@ -38,11 +37,8 @@         notifyItemChanged(position)
     }
 
     fun clearSelection() {
-        val selection = getSelectedItems()
         selectedItems.clear()
-        for (i in selection) {
-            notifyItemChanged(i)
-        }
+        notifyDataSetChanged()
     }
 
     fun getSelectedItemCount() = selectedItems.size()
@@ -61,7 +57,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 +71,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
@@ -109,6 +105,14 @@         val inflater = LayoutInflater.from(context)
 
         val rowView = inflater.inflate(R.layout.row_favourite, parent, false)
         return ViewHolder(rowView, onClickListener)
+    }
+
+    fun indexOf(name: String): Int {
+        return favourites.indexOf(name)
+    }
+
+    operator fun get(index: String): Favourite? {
+        return favourites[index]
     }
 
     class ViewHolder(itemView: View, private val listener: OnClickListener) : RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener {




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




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




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




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




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




diff --git a/app/src/main/play/de-DE/listing/phoneScreenshots/StopActivity.png b/app/src/main/play/de-DE/listing/phoneScreenshots/StopActivity.png
index ca0c078c9290838696b65e1672443186f8dcc7e0..6cde859fa2cdc925aa05d2d04158bfae0eb34301 100644
Binary files a/app/src/main/play/de-DE/listing/phoneScreenshots/StopActivity.png and b/app/src/main/play/de-DE/listing/phoneScreenshots/StopActivity.png differ




diff --git a/app/src/main/play/de-DE/listing/phoneScreenshots/StopSpecify.png b/app/src/main/play/de-DE/listing/phoneScreenshots/StopSpecify.png
index 92a38f8cb0d7c4947705252470745f93ca30d885..86c00aa109026e9cb98759dc1825df3705a12589 100644
Binary files a/app/src/main/play/de-DE/listing/phoneScreenshots/StopSpecify.png and b/app/src/main/play/de-DE/listing/phoneScreenshots/StopSpecify.png differ




diff --git a/app/src/main/play/de-DE/listing/phoneScreenshots/modification.png b/app/src/main/play/de-DE/listing/phoneScreenshots/modification.png
index 325188c99fca92bb5d52b00025b0d7a81d8cab27..c3648d2dca7047ec3d3f8d5e83047d8a97bf593f 100644
Binary files a/app/src/main/play/de-DE/listing/phoneScreenshots/modification.png and b/app/src/main/play/de-DE/listing/phoneScreenshots/modification.png differ




diff --git a/app/src/main/play/de-DE/whatsnew b/app/src/main/play/de-DE/whatsnew
index 08ac12d40165f105c20e3da48cae7ec7b66e0a85..cbd4b8733278f81740a965e5c432ce3ec40c15fe 100644
--- a/app/src/main/play/de-DE/whatsnew
+++ b/app/src/main/play/de-DE/whatsnew
@@ -1,6 +1,8 @@
 - offizieller Fahrplan von ZTM
+- VM kann ohne Offline-Fahrplan verwendet werden
+- Offline-Fahrplan verwendet genaue Daten (statt Arbeitstagen/Samstagen/Feiertagen)
 - VM ist schneller und mehr zuverlässig (da es genauso berechnet wird wie Offline-Abfahrten)
 - Lieblingshaltestellen von Grund auf umgeschrieben
 - App ist auf externen Speicher bewegbar
 - neue Farben — grau und grün — die zum neuen Posener Stil passen
-- mehrere Fehler behoben 
+- mehrere Fehler behoben




diff --git a/app/src/main/play/en-GB/listing/phoneScreenshots/StopActivity.png b/app/src/main/play/en-GB/listing/phoneScreenshots/StopActivity.png
index 7ebd7846e24ddd62c650e885df31cb1f4206ae3b..a85d7e9dfdcb7b81b3a8a7d5097923c885356065 100644
Binary files a/app/src/main/play/en-GB/listing/phoneScreenshots/StopActivity.png and b/app/src/main/play/en-GB/listing/phoneScreenshots/StopActivity.png differ




diff --git a/app/src/main/play/en-GB/listing/phoneScreenshots/modification.png b/app/src/main/play/en-GB/listing/phoneScreenshots/modification.png
index 702e7c0993cf83ede23321091947d75bb5790370..2f536117feeb2ef180b3be65e36fee4154c94e4f 100644
Binary files a/app/src/main/play/en-GB/listing/phoneScreenshots/modification.png and b/app/src/main/play/en-GB/listing/phoneScreenshots/modification.png differ




diff --git a/app/src/main/play/en-GB/listing/phoneScreenshots/stopSpecify.png b/app/src/main/play/en-GB/listing/phoneScreenshots/stopSpecify.png
index 8217c6d31d35b74ba85c6c008df803a56167d579..a67350f5939563ab794be2ec1ef89be4f6f5e5f5 100644
Binary files a/app/src/main/play/en-GB/listing/phoneScreenshots/stopSpecify.png and b/app/src/main/play/en-GB/listing/phoneScreenshots/stopSpecify.png differ




diff --git a/app/src/main/play/en-GB/whatsnew b/app/src/main/play/en-GB/whatsnew
index 90bdcdb797fa4283af773200af88790e233fbb7b..177b822ae2ab045622785f35ecda675d18fd48c9 100644
--- a/app/src/main/play/en-GB/whatsnew
+++ b/app/src/main/play/en-GB/whatsnew
@@ -1,4 +1,6 @@
 - official timetable from ZTM
+- VM can be used without offline timetable
+- offline timetable uses exact dates (instead of workdays/saturdays/holidays)
 - VM is quicker and is more reliable (as it’s computed in the same way as offline departures)
 - favourites rewritten from scratch
 - app is movable to external storage




diff --git a/app/src/main/play/en-US/listing/phoneScreenshots/StopActivity.png b/app/src/main/play/en-US/listing/phoneScreenshots/StopActivity.png
index 7ebd7846e24ddd62c650e885df31cb1f4206ae3b..a85d7e9dfdcb7b81b3a8a7d5097923c885356065 100644
Binary files a/app/src/main/play/en-US/listing/phoneScreenshots/StopActivity.png and b/app/src/main/play/en-US/listing/phoneScreenshots/StopActivity.png differ




diff --git a/app/src/main/play/en-US/listing/phoneScreenshots/modification.png b/app/src/main/play/en-US/listing/phoneScreenshots/modification.png
index 702e7c0993cf83ede23321091947d75bb5790370..2f536117feeb2ef180b3be65e36fee4154c94e4f 100644
Binary files a/app/src/main/play/en-US/listing/phoneScreenshots/modification.png and b/app/src/main/play/en-US/listing/phoneScreenshots/modification.png differ




diff --git a/app/src/main/play/en-US/listing/phoneScreenshots/stopSpecify.png b/app/src/main/play/en-US/listing/phoneScreenshots/stopSpecify.png
index 8217c6d31d35b74ba85c6c008df803a56167d579..a67350f5939563ab794be2ec1ef89be4f6f5e5f5 100644
Binary files a/app/src/main/play/en-US/listing/phoneScreenshots/stopSpecify.png and b/app/src/main/play/en-US/listing/phoneScreenshots/stopSpecify.png differ




diff --git a/app/src/main/play/en-US/whatsnew b/app/src/main/play/en-US/whatsnew
index 93ea29b4f529f8032860db46233be0133b909333..177b822ae2ab045622785f35ecda675d18fd48c9 100644
--- a/app/src/main/play/en-US/whatsnew
+++ b/app/src/main/play/en-US/whatsnew
@@ -1,6 +1,8 @@
 - official timetable from ZTM
+- VM can be used without offline timetable
+- offline timetable uses exact dates (instead of workdays/saturdays/holidays)
 - VM is quicker and is more reliable (as it’s computed in the same way as offline departures)
-- favorites rewritten from scratch
+- favourites rewritten from scratch
 - app is movable to external storage
-- new colors—gray and green—fitting new Poznań style
+- new colours—grey and green—fitting new Poznań style
 - multiple bug fixes




diff --git a/app/src/main/play/it-IT/listing/phoneScreenshots/StopActivity.png b/app/src/main/play/it-IT/listing/phoneScreenshots/StopActivity.png
index 2e15bc7054690c2255babfa8fb177fff2614843e..de1163003b4205b0c6063228029912d5830efd72 100644
Binary files a/app/src/main/play/it-IT/listing/phoneScreenshots/StopActivity.png and b/app/src/main/play/it-IT/listing/phoneScreenshots/StopActivity.png differ




diff --git a/app/src/main/play/it-IT/listing/phoneScreenshots/StopSpecify.png b/app/src/main/play/it-IT/listing/phoneScreenshots/StopSpecify.png
index 124ba54ea124aa35f2d1ed69d51927ee5e1daaa3..77d36a1d0fe4a002881d3206a6b1e1218562183d 100644
Binary files a/app/src/main/play/it-IT/listing/phoneScreenshots/StopSpecify.png and b/app/src/main/play/it-IT/listing/phoneScreenshots/StopSpecify.png differ




diff --git a/app/src/main/play/it-IT/listing/phoneScreenshots/modification.png b/app/src/main/play/it-IT/listing/phoneScreenshots/modification.png
index df0a4a35105043b3b535333d7b96d45659b992e7..defccce5b66623b7dc6d0e27aa415238fc11d70b 100644
Binary files a/app/src/main/play/it-IT/listing/phoneScreenshots/modification.png and b/app/src/main/play/it-IT/listing/phoneScreenshots/modification.png differ




diff --git a/app/src/main/play/it-IT/whatsnew b/app/src/main/play/it-IT/whatsnew
index 5bba7d071638f7a133ed4d05fe26b64e4a9ff343..c5e305a6e9aa21fb45523afd12e177be5c6c279e 100644
--- a/app/src/main/play/it-IT/whatsnew
+++ b/app/src/main/play/it-IT/whatsnew
@@ -1,4 +1,6 @@
 - ufficiale orario da ZTM
+- VM può essere utilizzato senza orario offline
+- l’orario offline utilizza le date esatte (invece di giorni lavorativi/sabato/festività)
 - VM è più veloce e più affidabile (in quanto è calcolato allo stesso modo delle partenze offline)
 - favoriti rescritti da capo
 - app è trasferibile su una memoria esterna




diff --git a/app/src/main/play/pl-PL/listing/phoneScreenshots/StopActivity.png b/app/src/main/play/pl-PL/listing/phoneScreenshots/StopActivity.png
index 6a7addd5e3bc04fb6fe5676933d9f8dc8a56f342..4df78339578128289b152e676c58051fded2c12f 100644
Binary files a/app/src/main/play/pl-PL/listing/phoneScreenshots/StopActivity.png and b/app/src/main/play/pl-PL/listing/phoneScreenshots/StopActivity.png differ




diff --git a/app/src/main/play/pl-PL/listing/phoneScreenshots/StopSpecify.png b/app/src/main/play/pl-PL/listing/phoneScreenshots/StopSpecify.png
index e4430fd9d8cfb0a2b001aeaa0d1b409c99745255..67b81dc87ef810057208d330df299de0a3f9bd50 100644
Binary files a/app/src/main/play/pl-PL/listing/phoneScreenshots/StopSpecify.png and b/app/src/main/play/pl-PL/listing/phoneScreenshots/StopSpecify.png differ




diff --git a/app/src/main/play/pl-PL/listing/phoneScreenshots/modification.png b/app/src/main/play/pl-PL/listing/phoneScreenshots/modification.png
index f909547736638f297ad26ddfabf2162224dde75a..f15b3b0ab78817a7c0ab9aeff43dcdb034e7e8cb 100644
Binary files a/app/src/main/play/pl-PL/listing/phoneScreenshots/modification.png and b/app/src/main/play/pl-PL/listing/phoneScreenshots/modification.png differ




diff --git a/app/src/main/play/pl-PL/whatsnew b/app/src/main/play/pl-PL/whatsnew
index a2a83d24b5331ec136609bbf071f29604dea93ac..4133aac968c67993723579f25b0b86495d58850c 100644
--- a/app/src/main/play/pl-PL/whatsnew
+++ b/app/src/main/play/pl-PL/whatsnew
@@ -1,4 +1,6 @@
 - oficjalny rozkład od ZTM
+- VM może być używany bez rozkładu offline
+- rozkład offline operuje na dokładnych datach (zamiast dni powszednie/soboty/święta)
 - VM jest szybszy i pewniejszy (ponieważ jest generowany w ten sam sposób, co odjazdy offline)
 - ulubione przepisane od zera
 - aplikacja może być przenoszona do pamięci zewnętrznej




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




diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
deleted file mode 100644
index c7bd21dbd86990cde81fea8abd3bf904b4546749..0000000000000000000000000000000000000000
--- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:aapt="http://schemas.android.com/aapt"
-    android:width="108dp"
-    android:height="108dp"
-    android:viewportHeight="108"
-    android:viewportWidth="108">
-    <path
-        android:fillType="evenOdd"
-        android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
-        android:strokeColor="#00000000"
-        android:strokeWidth="1">
-        <aapt:attr name="android:fillColor">
-            <gradient
-                android:endX="78.5885"
-                android:endY="90.9159"
-                android:startX="48.7653"
-                android:startY="61.0927"
-                android:type="linear">
-                <item
-                    android:color="#44000000"
-                    android:offset="0.0" />
-                <item
-                    android:color="#00000000"
-                    android:offset="1.0" />
-            </gradient>
-        </aapt:attr>
-    </path>
-    <path
-        android:fillColor="#FFFFFF"
-        android:fillType="nonZero"
-        android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
-        android:strokeColor="#00000000"
-        android:strokeWidth="1" />
-</vector>




diff --git a/app/src/main/res/layout/activity_edit_favourite.xml b/app/src/main/res/layout/activity_edit_favourite.xml
index b93a989607cf125b465c9631cbb173bec3502693..8788c2dcff597cb72bf74e8478c1040bde28a941 100644
--- a/app/src/main/res/layout/activity_edit_favourite.xml
+++ b/app/src/main/res/layout/activity_edit_favourite.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/dialog_favourite"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -38,28 +39,36 @@              android:id="@+id/favourite_name_edit"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
         android:layout_marginStart="8dp"
         android:ems="10"
         android:inputType="text"
         android:text=""
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/name_label"
         app:layout_constraintEnd_toEndOf="parent"
-        android:layout_marginEnd="8dp" />
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/name_label" />
 
     <android.support.v7.widget.RecyclerView
         android:id="@+id/favourite_edit_list"
         android:layout_width="0dp"
         android:layout_height="0dp"
-        android:layout_marginBottom="8dp"
-        android:layout_marginEnd="8dp"
-        android:layout_marginStart="8dp"
         android:layout_marginTop="8dp"
+        android:visibility="gone"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@+id/favourite_name_edit">
 
     </android.support.v7.widget.RecyclerView>
+
+    <ProgressBar
+        android:id="@+id/favourite_edit_loading"
+        style="@style/Widget.AppCompat.ProgressBar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="@+id/favourite_edit_list"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/favourite_name_edit" />
 
 </android.support.constraint.ConstraintLayout>
\ No newline at end of file




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




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




diff --git a/app/src/main/res/layout/row_favourite_edit.xml b/app/src/main/res/layout/row_favourite_edit.xml
index 7de8a6a53aa15b716770b036dba2906bd855bfe1..1d48cfacec0056c94b5756ed01064e09d3de2e6e 100644
--- a/app/src/main/res/layout/row_favourite_edit.xml
+++ b/app/src/main/res/layout/row_favourite_edit.xml
@@ -28,15 +28,13 @@         android:id="@+id/favourite_edit_delete"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginBottom="16dp"
-        android:layout_marginStart="9dp"
+        android:layout_marginEnd="8dp"
         android:layout_marginTop="16dp"
+        android:contentDescription="@string/favourite_element_delete_button"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintTop_toTopOf="parent"
-        app:srcCompat="@drawable/ic_delete"
-        tools:layout_editor_absoluteX="335dp"
-        tools:layout_editor_absoluteY="16dp"
-        android:contentDescription="@string/favourite_element_delete_button" />
+        app:srcCompat="@drawable/ic_delete" />
 
     <!--<ImageView-->
         <!--android:id="@+id/favourite_edit_split"-->




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




diff --git a/app/src/main/res/menu/menu_drawer.xml b/app/src/main/res/menu/menu_drawer.xml
index b100a34f1b78517244b39dfb665156dc5f7a6b4d..3dbf30fa996dbd94345b8d176a0f110b1729033c 100644
--- a/app/src/main/res/menu/menu_drawer.xml
+++ b/app/src/main/res/menu/menu_drawer.xml
@@ -11,10 +11,10 @@                      android:id="@+id/drawer_refresh"
             android:icon="@drawable/ic_refresh"
             android:title="@string/refresh" />
-        <item
+        <!--<item
             android:id="@+id/drawer_help"
             android:icon="@drawable/ic_help"
-            android:title="@string/help" />
+            android:title="@string/help" />-->
         <item
             android:id="@+id/drawer_settings"
             android:icon="@drawable/ic_settings"




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




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 59bae428d441c18d3f8b73074d7e738ee2d1c448..513e4652283dcb5cd773feefafbc6ce810b49bcd 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -18,7 +18,8 @@     Downloading timetable
     <string name="timetable_downloading_progress" translatable="false">%1$1.2f MiB/%2$1.2f MiB</string>
     <string name="timetable_decompressing">Decompressing timetable</string>
     <string name="search_placeholder">Stop…</string>
-    <string name="no_connectivity">No connectivity – can’t update timetable</string>
+    <string name="no_connectivity_cant_update">No connectivity – can’t update timetable</string>
+    <string name="no_connectivity">No connectivity</string>
     <string name="timetable_up_to_date">Timetable is up-to-date</string>
     <string name="validity_failed">Downloaded timetable is corrupted – can’t update</string>
     <string name="error_try_later">Error. Try again later</string>
@@ -42,30 +43,18 @@     In %1$s minutes
     <string name="departure_in__plural_nominative">In %1$s minutes</string>
 
     <string name="home">Home</string>
-    <string name="refresh">Refresh</string>
+    <string name="refresh">Update timetable</string>
     <string name="help">Help</string>
     <string name="navigation_drawer_home_button" translatable="false">navigation drawer home button</string>
     <string name="navigation_drawer_refresh_button" translatable="false">navigation drawer refresh button</string>
     <string name="navigation_drawer_help_button" translatable="false">navigation drawer help button</string>
     <string name="title_activity_help">Help</string>
     <string name="help_text">
-        "Why is there no timetable for Saturdays?\n\n"
-
-        "Current official timetable will have been invalid by Saturday so it doesn’t contain times"
-        "for Saturdays.\n"
-        "Be sure to update timetable on Saturday at the latest.\n\n"
-
         "Why a favourite card shows ‘No next departure’?\n\n"
 
         "Favourite cards contain times for today and tomorrow."
         "‘No next departure’ may happen when, e.g. there is no timetable for tomorrow and there"
         "are no more departures today.\n\n"
-
-        "Today is Tuesday but it’s a holiday. Shouldn’t I look at Sundays timetable?\n\n"
-
-        "Official timetable is especially prepared for holidays—it will show up as today (if"
-        "it’s Tuesday, it will be on ‘workdays’ tab).\n"
-        "Be sure to consult the messages on\nhttps://www.ztm.poznan.pl/en.\n\n"
     </string>
     <string name="departure_row_getting_departures">Getting departures…</string>
     <string name="valid_since">Valid since %1$s</string>
@@ -90,4 +79,16 @@     key_timetable_source_url
     <string name="title_timetable_source_url">Timetable source</string>
     <string name="title_activity_settings">Settings</string>
     <string name="settings">Settings</string>
+    <string name="key_timetable_automatic_update" translatable="false">key_timetable_automatic_update</string>
+    <string name="title_timetable_automatic_update">Automatic updates</string>
+
+    <string name="Mon">Mon</string>
+    <string name="Tue">Tue</string>
+    <string name="Wed">Wed</string>
+    <string name="Thu">Thu</string>
+    <string name="Fri">Fri</string>
+    <string name="Sat">Sat</string>
+    <string name="Sun">Sun</string>
+    <string name="summary_timetable_automatic_update">Automatically check for and download timetable updates</string>
+    <string name="server_error">Server error</string>
 </resources>




diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 74a1403bc6cf2643facca4506c5f1b23cd015a27..094229af232761e69b00552d2a3022dca6161e84 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -17,7 +17,8 @@     Verbind mit dem Internet, um den Fahrplan herunterzuladen
     <string name="no_db_downloading">Fahrplan wird heruntergeladen…</string>
     <string name="timetable_downloading">Fahrplan wird heruntergeladen</string>
     <string name="search_placeholder">Haltestelle…</string>
-    <string name="no_connectivity">Kein Verbindung – kann nicht den Fahrplan aktualisieren</string>
+    <string name="no_connectivity_cant_update">Kein Verbindung – kann nicht den Fahrplan aktualisieren</string>
+    <string name="no_connectivity">Kein Verbindung</string>
     <string name="timetable_up_to_date">Fahrplan ist aktuell</string>
     <string name="validity_failed">Der heruntergeladene Fahrplan ist geschädigt – kann nicht aktualisieren</string>
     <string name="error_try_later">Fehler. Versuch später noch einmal</string>
@@ -33,30 +34,18 @@     Zusammenfügen
     <string name="merge_favourites">Lieblingshaltestellen zusammenfügen</string>
     <string name="loading">Laden…</string>
     <string name="home">Startseite</string>
-    <string name="refresh">Erfrischen</string>
+    <string name="refresh">Fahrplan aktualisieren</string>
     <string name="help">Hilfe</string>
     <string name="title_activity_help">Hilfe</string>
     <string name="valid_since">Gilt seit %1$s</string>
     <string name="valid_till">Gilt bis %1$s</string>
     <string name="departure_row_getting_departures">Abfahrten sammeln…</string>
     <string name="help_text">
-        "Warum gibt es keinen Fahrplan für Samstag?\n\n"
-
-        "Der aktuelle offizielle Fahrplan wird bis Samstag ungültig sein also es enthält nicht den"
-        "Abfahrtszeiten für Samstag.\n"
-        "Sei sicher, den Fahrplan bis spätestens Samstag zu aktualisieren.\n\n"
-
         "Warum Lieblingshaltestelle zeigt „Keine nächste Abfahrt“?\n\n"
 
         "Die Lieblingshaltestellen enthalten Abfahrtszeiten für heute und morgen."
         "„Keine nächste Abfahrt“ kann passieren, wenn es z.B. keinen Zeitplan für"
         "morgen gibt und heute keine Abfahrten mehr stattfinden.\n\n"
-
-        "Heute ist Dienstag, aber es ist ein Feiertag. Soll ich nicht den Sonntags Fahrplan angucken?\n\n"
-
-        "Der offizielle Fahrplan ist vorbereitet besonders für Feiertagen — es wird sich als"
-        "heute zeigen (wenn es Dienstag ist, es wird auf „Arbeitstage“ Karte sein).\n"
-        "Sei sicher, die Nachrichten auf\nhttps://www.ztm.poznan.pl/en\nkonsultieren.\n\n"
     </string>
     <string name="refreshing_cache">Cache wird aktualisiert. Es kann einige Zeit dauern…</string>
     <string name="today">Heute</string>
@@ -73,4 +62,14 @@     Quelle des Fahrplans
     <string name="title_activity_settings">Einstellungen</string>
     <string name="settings">Einstellungen</string>
     <string name="timetable_decompressing">Fahrplan wird entpackt</string>
+    <string name="Mon">Mo.</string>
+    <string name="Tue">Di.</string>
+    <string name="Wed">Mi.</string>
+    <string name="Thu">Do.</string>
+    <string name="Fri">Fr.</string>
+    <string name="Sat">Sa.</string>
+    <string name="Sun">So.</string>
+    <string name="title_timetable_automatic_update">Automatische Updates</string>
+    <string name="summary_timetable_automatic_update">Automatisch nach Fahrplanaktualisierungen suchen und diese herunterladen</string>
+    <string name="server_error">Serverfehler</string>
 </resources>




diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index e18d6d65bf573b10800637ce426adbf393ea9bda..2269942ded34a1ddcc980175b7536da1b0b10ede 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -20,7 +20,8 @@     Connetti a Internet per scaricare l’orario
     <string name="no_db_downloading">L’orario è stando scaricato</string>
     <string name="timetable_downloading">Scaricando l’orario</string>
     <string name="search_placeholder">Fermata…</string>
-    <string name="no_connectivity">Nessuna connettività – non si riesce aggiornare l’orario</string>
+    <string name="no_connectivity_cant_update">Nessuna connettività – non si riesce aggiornare l’orario</string>
+    <string name="no_connectivity">Nessuna connettività</string>
     <string name="timetable_up_to_date">L’orario sta aggiornato</string>
     <string name="validity_failed">L’orario scaricato sta corrotto – non si riesce aggiornare</string>
     <string name="error_try_later">Errore. Riprova più tardi</string>
@@ -36,25 +37,14 @@     Nessuna partenza successiva
     <string name="action_merge">Unisci</string>
     <string name="merge_favourites">Unisci le favorite</string>
     <string name="loading">Caricamento in corso…</string>
-    <string name="refresh">Ricarica</string>
+    <string name="refresh">Aggiorna l’orario</string>
     <string name="departure_row_getting_departures">Ottenere le partenze…</string>
     <string name="help_text">
-        "Perché non c’è nessun orario per il sabato?\n\n"
-
-        "Attuale orario ufficiale sarà invalido entro sabato perciò non contiene ore per sabato.\n"
-        "Assicurati di aggiornare l’orario il sabato al più tardi\n\n"
-
         "Perché un favorito mostra «Nessuna partenza successiva»?\n\n"
         
         "I favoriti contengono ore per oggi e domani."
         "«Nessuna partenza successiva» potrebbe succedere quando, ad esempio, non c’e orario per"
         "domani e non ci sono partenze oggi in più.\n\n" 
-
-        "Oggi è martedì però è un giorno festivo. Dovrei guardare l’orario per la domenica?\n\n"
-
-        "L’orario ufficiale è preparato specialmente per giorni festivi – sarà mostrato come oggi"
-        "(se è martedì, sarà nella scheda «giorni di lavoro»).\n"
-        "Assicurati di consultare le notifiche su\nhttps://www.ztm.poznan.pl/en.\n\n"
     </string>
     <string name="refreshing_cache">Cache sta essendo aggiornato. Può richiedere un certo tempo…</string>
     <string name="today">Oggi</string>
@@ -71,4 +61,14 @@     Fonte dell’Orario
     <string name="title_activity_settings">Impostazioni</string>
     <string name="settings">Impostazioni</string>
     <string name="timetable_decompressing">Decomprimendo l’orario</string>
+    <string name="Mon">lun</string>
+    <string name="Tue">mar</string>
+    <string name="Wed">mer</string>
+    <string name="Thu">gio</string>
+    <string name="Fri">ven</string>
+    <string name="Sat">sab</string>
+    <string name="Sun">dom</string>
+    <string name="summary_timetable_automatic_update">Controlla e scarica automaticamente gli aggiornamenti dell’orario</string>
+    <string name="title_timetable_automatic_update">Aggiornamenti automatici</string>
+    <string name="server_error">Errore del server</string>
 </resources>




diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 3693fadac99d212054a413ec878bf37f093d5c77..e3a00a950c096b82a64501d0a55b6649e1cccce8 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -12,7 +12,8 @@     Maak verbinding met internet om de dienstregeling te downloaden.
     <string name="no_db_downloading">De dienstregeling wordt gedownload…</string>
     <string name="timetable_downloading">Bezig met downloaden van dienstregeling</string>
     <string name="search_placeholder">Halte…</string>
-    <string name="no_connectivity">Geen internetverbinding – de dienstregeling kan niet worden bijgewerkt.</string>
+    <string name="no_connectivity_cant_update">Geen internetverbinding – de dienstregeling kan niet worden bijgewerkt.</string>
+    <string name="no_connectivity">Geen internetverbinding</string>
     <string name="timetable_up_to_date">De dienstregeling is volledig bijgewerkt.</string>
     <string name="validity_failed">De gedownloade dienstregeling bevat fouten – bijwerken is niet mogelijk.</string>
     <string name="error_try_later">Fout; probeer het later opnieuw.</string>
@@ -33,27 +34,15 @@     Over %1$s minuten
     <string name="departure_in__plural_nominative">Over %1$s minuten</string>
 
     <string name="home">Startpagina</string>
-    <string name="refresh">Verversen</string>
+    <string name="refresh">Dienstregeling bijwerken</string>
     <string name="help">Hulp</string>
     <string name="title_activity_help">Hulp</string>
     <string name="help_text">
-        "Waarom is er geen zaterdagdienstregeling?\n\n"
-
-        "De huidige dienstregeling zal zaterdag niet meer kloppen; daarom bevat deze geen tijden"
-        "voor zaterdag.\n"
-        "Zorg ervoor dat je uiterlijk zaterdag de dienstregeling bijwerkt.\n\n"
-
         "Waarom toont een favoriet ‘Geen vertrektijden’?\n\n"
 
         "Favorieten tonen tijden voor vandaag en morgen."
         "‘Geen vertrektijden’ kan worden getoond als er bijv. geen dienstregeling voor morgen is en"
         "er vandaag geen voertuigen meer vertrekken.\n\n"
-
-        "Het is vandaag niet alleen dinsdag, maar ook een feestdag. Moet ik niet naar de zondagsdienstregeling kijken?\n\n"
-
-        "De dienstregeling is voorbereid op feestdagen; deze zal worden weergegeven als vandaag (als"
-        "het dinsdag is, dan zal deze zich bevinden op het tabblad ‘werkdagen’).\n"
-        "Lees ook de berichten op\nhttps://www.ztm.poznan.pl/en.\n\n"
     </string>
     <string name="departure_row_getting_departures">Bezig met ophalen van vertrektijden…</string>
     <string name="valid_since">Ingegaan op %1$s</string>
@@ -74,4 +63,14 @@     Bron van de dienstregeling
     <string name="title_activity_settings">Instellingen</string>
     <string name="settings">Instellingen</string>
     <string name="timetable_decompressing">Bezig met uitpakken van dienstregeling</string>
+    <string name="Mon">ma</string>
+    <string name="Tue">di</string>
+    <string name="Wed">wo</string>
+    <string name="Thu">do</string>
+    <string name="Fri">vr</string>
+    <string name="Sat">za</string>
+    <string name="Sun">zo</string>
+    <string name="title_timetable_automatic_update">Automatische updates</string>
+    <string name="summary_timetable_automatic_update">Automatisch controleren en download dienstregeling updates</string>
+    <string name="server_error">Serverfout</string>
 </resources>




diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 15370f10365138c4426f4c71c7ab5af1bc7370b0..d627a692c607c5efb7198a47e16ecda5d285f774 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -14,7 +14,8 @@     Pobieranie rozkładu
     <string name="search_placeholder">Przystanek…</string>
     <string name="timetable_up_to_date">Rozkład jest aktualny</string>
     <string name="validity_failed">Pobrany rozkład jest uszkodzony – nie można zaktualizować</string>
-    <string name="no_connectivity">Brak połączenia z Internetem – nie można zaktualizować rozkładu</string>
+    <string name="no_connectivity_cant_update">Brak połączenia z Internetem – nie można zaktualizować rozkładu</string>
+    <string name="no_connectivity">Brak połączenia z Internetem</string>
     <string name="error_try_later">Błąd. Spróbuj ponownie później</string>
     <string name="now">Teraz</string>
     <string name="stop_already_fav">Ten przystanek już jest pośród ulubionych</string>
@@ -33,27 +34,15 @@     Za %1$s minutę
     <string name="departure_in__plural_genitive">Za %1$s minut</string>
     <string name="departure_in__plural_nominative">Za %1$s minuty</string>
     <string name="home">Strona główna</string>
-    <string name="refresh">Odśwież</string>
+    <string name="refresh">Zaktualizuj rozkład</string>
     <string name="help">Pomoc</string>
     <string name="title_activity_help">Pomoc</string>
     <string name="help_text">
-        "Dlaczego nie ma rozkładu na sobotę?\n\n"
-
-        "Aktualny oficjalny rozkład będzie nieważny przed sobotą, więc nie zawiera czasów dla"
-        "soboty.\n"
-        "Pamiętaj, aby zaktualizować rozkład najpóźniej w sobotę.\n\n"
-
         "Dlaczego ulubiony pokazuje „Brak następnego odjazdu”?\n\n"
 
         "Ulubione zawierają czasy dla dzisiaj i jutra."
         "„Brak następnego odjazdu” może się wydarzyć, np. gdy nie ma rozkładu na jutro, a dzisiaj"
         "nie ma już odjazdów.\n\n"
-
-        "Dziś jest wtorek, ale jest święto. Nie powinienem patrzeć na rozkład dla niedzieli?\n\n"
-
-        "Oficjalny rozkład jest specjalnie przygotowywany na święta — będzie widoczny w dzisiejszej"
-        "zakładce (jeśli jest wtorek, to w „dni robocze”).\n"
-        "Pamiętaj, aby sprawdzić aktualności na\nhttps://www.ztm.poznan.pl.\n\n"
     </string>
     <string name="departure_row_getting_departures">Zbieranie odjazdów…</string>
     <string name="valid_since">Ważny od %1$s</string>
@@ -73,4 +62,14 @@     Źródło rozkładu
     <string name="title_activity_settings">Ustawienia</string>
     <string name="settings">Ustawienia</string>
     <string name="timetable_decompressing">Rozpakowywanie rozkładu</string>
+    <string name="Mon">pon.</string>
+    <string name="Tue">wt.</string>
+    <string name="Wed">śr.</string>
+    <string name="Thu">czw.</string>
+    <string name="Fri">pt.</string>
+    <string name="Sat">sob.</string>
+    <string name="Sun">niedz.</string>
+    <string name="summary_timetable_automatic_update">Automatycznie sprawdzaj i pobieraj aktualizacje rozkładu</string>
+    <string name="title_timetable_automatic_update">Automatyczne aktualizacje</string>
+    <string name="server_error">Błąd servera</string>
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml
index 1c3647edb4ef835066979a1174d44dbd79359557..e35091e5b4688366446865e6123a4cfe880fd70b 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="@string/key_timetable_automatic_update"
+            android:summary="@string/summary_timetable_automatic_update"
+            android:title="@string/title_timetable_automatic_update" />
     </PreferenceCategory>
 </PreferenceScreen>




diff --git a/build.gradle b/build.gradle
index abd71c41b0e3809a905d7e36ce37c4c9b60fb0b2..46a60aa5ed0c08de693c8ed41f436fa47d145972 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.70'
     repositories {
         jcenter()
         maven { url 'https://maven.google.com' }
@@ -9,7 +9,7 @@         //maven { url 'https://dl.bintray.com/guardian/android' } // TooLargeTool
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.1.3'
+        classpath 'com.android.tools.build:gradle:3.1.4'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 
         // NOTE: Do not place your application dependencies here; they belong




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




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