Bimba.git

commit 24a663e882ae736c035e20e8b69a9867f66d51ff

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

favourites: adding, deleting splitting

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


diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 907ec3e2fdfe60bc23b6a159134f22cc61a30d06..76deb8315581a75e1235f7196a8a1cc9da3300fe 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -34,7 +34,9 @@         
 
         <service
             android:name=".VmClient"
-            android:exported="false"></service>
+            android:exported="false" />
+
+        <activity android:name=".activities.EditFavouriteActivity"></activity>
     </application>
 
 </manifest>
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt b/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt
index b6be3d8580f718b0ba98bb18736c8e94b93f4bff..fad8a2b196fa98dd6cd2127896847e578a146206 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/TimetableDownloader.kt
@@ -27,7 +27,7 @@             if (!isNetworkAvailable(this)) {
                 sendResult("no connectivity")
                 return
             }
-            val metadataUrl = URL("https://adamsprogs.ml/w/_media/programmes/bimba/timetable.db.meta")
+            val metadataUrl = URL("https://adamsprogs.ml/w/_media/programmes/bimba/timetable.meta")
             var httpCon = metadataUrl.openConnection() as HttpURLConnection
             if (httpCon.responseCode != HttpURLConnection.HTTP_OK)
                 throw Exception("Failed to connect")
@@ -36,7 +36,13 @@             val reader = BufferedReader(InputStreamReader(httpCon.inputStream))
             val lastModified = reader.readLine()
             val checksum = reader.readLine()
             size = Integer.parseInt(reader.readLine()) / 1024
-            val currentLastModified = prefs.getString("timetableLastModified", "1979-10-12T00:00")
+            val dbVersion = reader.readLine()
+            if (Integer.parseInt(dbVersion.split(".")[0]) > 1) { //todo version to const
+                sendResult("version mismatch")
+                return
+            }
+            val dbFilename = reader.readLine()
+            val currentLastModified = prefs.getString("timetableLastModified", "19791012")
             if (lastModified <= currentLastModified && !intent.getBooleanExtra("force", false)) {
                 sendResult("up-to-date")
                 return
@@ -45,7 +51,7 @@             Log.i("Downloader", "timetable is newer ($lastModified > $currentLastModified)")
 
             notify(0)
 
-            val xzDbUrl = URL("https://adamsprogs.ml/w/_media/programmes/bimba/timetable.db.xz")
+            val xzDbUrl = URL("https://adamsprogs.ml/w/_media/programmes/bimba/$dbFilename")
             httpCon = xzDbUrl.openConnection() as HttpURLConnection
             if (httpCon.responseCode != HttpURLConnection.HTTP_OK)
                 throw Exception("Failed to connect")




diff --git a/app/src/main/java/ml/adamsprogs/bimba/VmClient.kt b/app/src/main/java/ml/adamsprogs/bimba/VmClient.kt
index 0592a803d2eff1eff334b3e9c5b15505809a9178..77a60772f12835221c39d198b292738d3f4ffe00 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/VmClient.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/VmClient.kt
@@ -15,10 +15,10 @@     override fun onHandleIntent(intent: Intent?) {
         if (intent != null) {
             val stopId = intent.getStringExtra("stopId")
             if (!isNetworkAvailable(this)) {
-                sendResult(createDepartures(this, stopId))
+                sendResult(createDepartures(stopId))
             } else {
                 val stopSymbol = intent.getStringExtra("stopSymbol")
-                val departures = createDepartures(this, stopId)
+                val departures = createDepartures(stopId)
 
                 val client = OkHttpClient()
                 val url = "http://www.peka.poznan.pl/vm/method.vm?ts=${Calendar.getInstance().timeInMillis}"




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 5e7e894d5d329e49b287b8c16c8a34790b64e7aa..6f068964f6951e6093281d7a1e485bb823890c97 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt
@@ -15,12 +15,10 @@ import android.support.v4.widget.*
 import android.support.v7.widget.*
 import android.util.Log
 import android.view.inputmethod.InputMethodManager
-import com.google.gson.Gson
-import com.google.gson.JsonObject
 import ml.adamsprogs.bimba.*
 
 //todo refresh every 15s
-class DashActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, SwipeRefreshLayout.OnRefreshListener {
+class DashActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, SwipeRefreshLayout.OnRefreshListener, FavouritesAdapter.OnMenuItemClickListener {
     val context: Context = this
     val receiver = MessageReceiver()
     lateinit var timetable: Timetable
@@ -28,6 +26,7 @@     var stops: ArrayList? = null
     lateinit var swipeRefreshLayout: SwipeRefreshLayout
     lateinit var favouritesList: RecyclerView
     lateinit var searchView: FloatingSearchView
+    lateinit var favourites: FavouriteStorage
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -49,6 +48,7 @@         searchView.setOnFocusChangeListener(object : FloatingSearchView.OnFocusChangeListener {
             override fun onFocus() {
                 swipeRefreshLayout.isEnabled = false
                 favouritesList.visibility = View.GONE
+                //todo show suggestions
             }
 
             override fun onFocusCleared() {
@@ -100,32 +100,15 @@         //todo searchView.attachNavigationDrawerToMenuButton(mDrawerLayout)
     }
 
     private fun prepareFavourites() {
+        favourites = FavouriteStorage(context)
         val layoutManager = LinearLayoutManager(context)
         favouritesList = findViewById(R.id.favouritesList) as RecyclerView
-        favouritesList.adapter = FavouritesAdapter(context, getFavourites())
+        favouritesList.adapter = FavouritesAdapter(context, favourites.favouritesList, this)
         favouritesList.layoutManager = layoutManager
     }
 
-    private fun getFavourites(): ArrayList<Favourite> {
-        val preferences = context.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE)
-        val favouritesString = preferences.getString("favourites", "{}")
-        val favouritesMap = Gson().fromJson(favouritesString, JsonObject::class.java)
-        val favourites = ArrayList<Favourite>()
-        for ((name, jsonTimetables) in favouritesMap.entrySet()) {
-            val timetables = ArrayList<HashMap<String, String>>()
-            for (jsonTimetable in jsonTimetables.asJsonArray) {
-                val timetable = HashMap<String, String>()
-                timetable["stop"] = jsonTimetable.asJsonObject["stop"].asString
-                timetable["line"] = jsonTimetable.asJsonObject["line"].asString
-                timetables.add(timetable)
-            }
-            favourites.add(Favourite(name, timetables, context))
-        }
-        return favourites
-    }
-
     private fun getStops() {
-        timetable = Timetable(this)
+        timetable = getTimetable(this)
         stops = timetable.getStops()
     }
 
@@ -161,7 +144,8 @@     }
 
     override fun onResume() {
         super.onResume()
-        favouritesList.adapter = FavouritesAdapter(context, getFavourites())
+        favourites.refresh()
+        favouritesList.adapter = FavouritesAdapter(context, favourites.favouritesList, this)
         favouritesList.adapter.notifyDataSetChanged()
     }
 
@@ -169,7 +153,6 @@     override fun onDestroy() {
         super.onDestroy()
         receiver.removeOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener)
         unregisterReceiver(receiver)
-        timetable.close()
     }
 
     fun deAccent(str: String): String {
@@ -199,5 +182,22 @@         timetable.refresh()
         stops = timetable.getStops()
         Snackbar.make(swipeRefreshLayout, message, Snackbar.LENGTH_LONG).show()
         swipeRefreshLayout.isRefreshing = false
+    }
+
+    override fun edit(name: String): Boolean {
+        val intent = Intent(this, EditFavouriteActivity::class.java)
+        intent.putExtra("favourite", favourites.favourites[name])
+        startActivity(intent)
+        favourites.refresh()
+        (favouritesList.adapter as FavouritesAdapter).favourites = favourites.favouritesList
+        favouritesList.adapter.notifyDataSetChanged()
+        return true
+    }
+
+    override fun delete(name: String): Boolean {
+        favourites.delete(name)
+        (favouritesList.adapter as FavouritesAdapter).favourites = favourites.favouritesList
+        favouritesList.adapter.notifyDataSetChanged()
+        return 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
new file mode 100644
index 0000000000000000000000000000000000000000..b8380a37e4ce6a65d4b9f79882aa9f9139cf7c99
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt
@@ -0,0 +1,35 @@
+package ml.adamsprogs.bimba.activities
+
+import android.support.v7.app.AppCompatActivity
+import android.os.Bundle
+import android.support.v7.widget.DividerItemDecoration
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.support.v7.widget.Toolbar
+import android.widget.EditText
+import ml.adamsprogs.bimba.R
+import ml.adamsprogs.bimba.models.Favourite
+import ml.adamsprogs.bimba.models.FavouriteEditRowAdapter
+
+class EditFavouriteActivity : AppCompatActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_edit_favourite)
+
+        val favourite = intent.getParcelableExtra<Favourite>("favourite")
+
+        val recyclerView = findViewById(R.id.favourite_edit_list) as RecyclerView?
+        val layoutManager = LinearLayoutManager(this)
+        recyclerView!!.layoutManager = layoutManager
+        val dividerItemDecoration = DividerItemDecoration(this, layoutManager.orientation)
+        recyclerView.addItemDecoration(dividerItemDecoration)
+        recyclerView.adapter = FavouriteEditRowAdapter(this, favourite)
+        val toolbar = findViewById(R.id.toolbar) as Toolbar
+        setSupportActionBar(toolbar)
+        supportActionBar?.title = getString(R.string.edit_favourite_title, favourite.name)
+
+        val nameEdit = findViewById(R.id.favourite_name_edit) as EditText
+        nameEdit.setText(favourite.name)
+    }
+}




diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
index 01d759ab8d045afdaa50fc9d544e0b5c641e7dea..84182ce84b7908f81cd6836fdadcd6745b47f68b 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt
@@ -17,8 +17,7 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_nodb)
-        var filter: IntentFilter
-        filter = IntentFilter("ml.adamsprogs.bimba.timetableDownloaded")
+        var filter: IntentFilter = IntentFilter("ml.adamsprogs.bimba.timetableDownloaded")
         filter.addCategory(Intent.CATEGORY_DEFAULT)
         registerReceiver(timetableDownloadReceiver, filter)
         timetableDownloadReceiver.addOnTimetableDownloadListener(this)
@@ -30,6 +29,21 @@             filter = IntentFilter("android.net.conn.CONNECTIVITY_CHANGE")
             registerReceiver(networkStateReceiver, filter)
             networkStateReceiver.addOnConnectivityChangeListener(this)
         } else
+            downloadTimetable()
+    }
+
+    override fun onResume() {
+        super.onResume()
+        var filter: IntentFilter = IntentFilter("ml.adamsprogs.bimba.timetableDownloaded")
+        filter.addCategory(Intent.CATEGORY_DEFAULT)
+        registerReceiver(timetableDownloadReceiver, filter)
+        if (!isNetworkAvailable(this)) {
+            askedForNetwork = true
+            (findViewById(R.id.noDbCaption) as TextView).text = getString(R.string.no_db_connect)
+            filter = IntentFilter("android.net.conn.CONNECTIVITY_CHANGE")
+            registerReceiver(networkStateReceiver, filter)
+            networkStateReceiver.addOnConnectivityChangeListener(this)
+        } else if (!serviceRunning)
             downloadTimetable()
     }
 




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 d1c341f365785ba288f0dcb13ab2f1025de2fb29..91f260811253e612ba7d26f2d14570747932690a 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt
@@ -3,19 +3,20 @@
 import android.support.v7.app.AppCompatActivity
 import android.os.Bundle
 import android.content.Intent
-import ml.adamsprogs.bimba.models.Timetable
+import android.database.sqlite.SQLiteCantOpenDatabaseException
+import ml.adamsprogs.bimba.models.getTimetable
 
 
 class SplashActivity : AppCompatActivity() {
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        val timetable = Timetable(this)
-        if(timetable.isDatabaseHealthy())
+        try {
+            getTimetable(this)
             startActivity(Intent(this, DashActivity::class.java))
-        else
+        } catch(e: SQLiteCantOpenDatabaseException) {
             startActivity(Intent(this, NoDbActivity::class.java))
-        timetable.close()
+        }
         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 6fc79e3fde4c096a6231ef495ba2caf8543db131..ebdd6f4d74c1f8a06ebe87a7e5fc5f1d3ccab37a 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt
@@ -12,7 +12,7 @@ import android.support.v7.widget.*
 import android.support.v4.app.*
 import android.support.v4.view.*
 import android.support.v4.content.res.ResourcesCompat
-import com.google.gson.*
+import android.util.Log
 
 import ml.adamsprogs.bimba.models.*
 import ml.adamsprogs.bimba.*
@@ -49,13 +49,13 @@         createTimerTask()
 
         prepareOnDownloadListener()
 
-        timetable = Timetable(this)
+        timetable = getTimetable()
         supportActionBar?.title = timetable.getStopName(stopId) ?: "Stop"
 
         viewPager = findViewById(R.id.container) as ViewPager
         tabLayout = findViewById(R.id.tabs) as TabLayout
 
-        sectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager, createDepartures(this, stopId))
+        sectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager, createDepartures(stopId))
 
         viewPager!!.adapter = sectionsPagerAdapter
         viewPager!!.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabLayout))
@@ -67,26 +67,26 @@         scheduleRefresh()
 
         val fab = findViewById(R.id.fab) as FloatingActionButton
 
-        var favourites = Gson().fromJson(sharedPreferences.getString("favourites", "{}"), JsonObject::class.java)
-        if (favourites[stopSymbol] == null) {
+        val favourites = FavouriteStorage(context)
+        if (!favourites.has(stopSymbol)) {
             fab.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_favourite_empty, this.theme))
         }
 
         fab.setOnClickListener {
-            favourites = Gson().fromJson(sharedPreferences.getString("favourites", "{}"), JsonObject::class.java)
-            if (favourites[stopSymbol] == null) {
-                val items = JsonArray()
+            Log.i("FAB", "Click")
+            if (!favourites.has(stopSymbol)) {
+                Log.i("FAB", "Add")
+                val items = ArrayList<HashMap<String, String>>()
                 timetable.getLines(stopId)?.forEach {
-                    val o = JsonObject()
-                    o.addProperty("stop", stopId)
-                    o.addProperty("line", it)
+                    val o = HashMap<String, String>()
+                    o["stop"] = stopId
+                    o["line"] = it
                     items.add(o)
                 }
-                favourites.add(stopSymbol,items)
-                val favouritesString = Gson().toJson(favourites)
-                val editor = sharedPreferences.edit()
-                editor.putString("favourites", favouritesString)
-                editor.apply()
+                Log.i("FAB", "Say")
+                favourites.add(stopSymbol, items)
+                Log.i("FAB", "Change")
+                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)
                         .setAction("Action", null).show()
@@ -151,7 +151,7 @@                 timer.cancel()
             } else {
                 timetableType = "departure"
                 item.icon = (ResourcesCompat.getDrawable(resources, R.drawable.ic_timetable_full, this.theme))
-                sectionsPagerAdapter?.departures = createDepartures(this, stopId)
+                sectionsPagerAdapter?.departures = createDepartures(stopId)
                 sectionsPagerAdapter?.relativeTime = true
                 sectionsPagerAdapter?.notifyDataSetChanged()
                 scheduleRefresh()
@@ -167,7 +167,6 @@         super.onDestroy()
         receiver.removeOnVmListener(context as MessageReceiver.OnVmListener)
         unregisterReceiver(receiver)
         timer.cancel()
-        timetable.close()
     }
 
     class PlaceholderFragment : Fragment() {




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt
index a8b0c8e50301ad5526416091723a16c271574ed5..c60f6c604ea2beafda6575d042f6e3943e82fa9c 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt
@@ -33,8 +33,8 @@     }
     return filtered
 }
 
-fun createDepartures(context: Context, stopId: String): HashMap<String, ArrayList<Departure>> {
-    val timetable = Timetable(context)
+fun createDepartures(stopId: String): HashMap<String, ArrayList<Departure>> {
+    val timetable = getTimetable()
     val departures = timetable.getStopDepartures(stopId)
     val moreDepartures = timetable.getStopDepartures(stopId)
     val rolledDepartures = HashMap<String, ArrayList<Departure>>()
@@ -48,8 +48,6 @@         rolledDepartures[mode] = (departures[mode] as ArrayList +
                 moreDepartures[mode] as ArrayList<Departure>) as ArrayList<Departure>
         rolledDepartures[mode] = filterDepartures(rolledDepartures[mode])
     }
-
-    timetable.close()
 
     return rolledDepartures
 }




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 7e89bd7184783c96401ef4cc490302041ae0a8d9..ed1e8b3182e8b86962679cf74c973054473e09c9 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt
@@ -1,10 +1,52 @@
 package ml.adamsprogs.bimba.models
 
 import android.content.Context
+import android.os.Parcel
+import android.os.Parcelable
+import android.util.Log
 import java.util.*
+import kotlin.collections.ArrayList
+import kotlin.collections.HashMap
 
-class Favourite(var name: String, var timetables: ArrayList<HashMap<String, String>>, context: Context) {
-    val timetable = Timetable(context)
+class Favourite : Parcelable {
+    lateinit var name:String
+    lateinit var timetables: ArrayList<HashMap<String, String>>
+    lateinit var context: Context
+
+    private constructor()
+
+    constructor(parcel: Parcel) {
+        val array = ArrayList<String>()
+        parcel.readStringList(array)
+        val timetables = ArrayList<HashMap<String, String>>()
+        for (row in array) {
+            val element = HashMap<String, String>()
+            element["stop"] = row.split("|")[0]
+            element["line"] = row.split("|")[1]
+            timetables.add(element)
+        }
+        this.name = parcel.readString()
+        this.timetables = timetables
+    }
+
+    constructor(name: String, timetables: ArrayList<HashMap<String, String>>) {
+        this.name = name
+        this.timetables = timetables
+    }
+
+    override fun describeContents(): Int {
+        return 105
+    }
+
+    override fun writeToParcel(dest: Parcel?, flags: Int) {
+        val parcel = timetables.map { "${it["stop"]}|${it["line"]}" }
+        dest?.writeStringList(parcel)
+        dest?.writeString(name)
+    }
+
+    val timetable = getTimetable()
+    val size: Int
+        get() = timetables.size
 
     var nextDeparture: Departure? = null
         get() {
@@ -37,4 +79,24 @@
             return minDeparture
         }
         private set
+
+    fun delete(stop: String, line: String) {
+        Log.i("ROW", "Favourite deleting $stop, $line")
+        val element = HashMap<String, String>()
+        element["stop"] = stop
+        element["line"] = line
+        val b = timetables.remove(element)
+        Log.i("ROW", "$b")
+        Log.i("ROW", timetables.toString())
+    }
+
+    companion object CREATOR : Parcelable.Creator<Favourite> {
+        override fun createFromParcel(parcel: Parcel): Favourite {
+            return Favourite(parcel)
+        }
+
+        override fun newArray(size: Int): Array<Favourite?> {
+            return arrayOfNulls(size)
+        }
+    }
 }




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a1666423bf48e79f9154afa02d545a9d2d8b19b3
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt
@@ -0,0 +1,53 @@
+package ml.adamsprogs.bimba.models
+
+import android.content.Context
+import android.support.v7.widget.RecyclerView
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import ml.adamsprogs.bimba.R
+
+class FavouriteEditRowAdapter(val context: Context, var favourite: Favourite) :
+        RecyclerView.Adapter<FavouriteEditRowAdapter.ViewHolder>() {
+    override fun getItemCount(): Int {
+        return favourite.size
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
+        val timetable = getTimetable()
+        val favourites = FavouriteStorage(context)
+        val favouriteElement = timetable.getFavouriteElement(favourite.timetables[position]["stop"]!!,
+                favourite.timetables[position]["line"]!!)
+        holder?.rowTextView?.text = favouriteElement
+        holder?.splitButton?.setOnClickListener {
+            favourites.detach(favourite.name, favourite.timetables[position]["stop"]!!,
+                    favourite.timetables[position]["line"]!!, favouriteElement!!)
+            favourite = favourites.favourites[favourite.name]!!
+            notifyDataSetChanged()
+        }
+        holder?.deleteButton?.setOnClickListener {
+            favourites.delete(favourite.name, favourite.timetables[position]["stop"]!!,
+                    favourite.timetables[position]["line"]!!)
+            favourite = favourites.favourites[favourite.name]!!
+            notifyDataSetChanged()
+        }
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
+        val context = parent?.context
+        val inflater = LayoutInflater.from(context)
+
+        val rowView = inflater.inflate(R.layout.row_favourite_edit, parent, false)
+        val viewHolder = ViewHolder(rowView)
+        return viewHolder
+    }
+
+    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+        val rowTextView = itemView.findViewById(R.id.favourite_edit_row) as TextView
+        val splitButton = itemView.findViewById(R.id.favourite_edit_split) as ImageView
+        val deleteButton = itemView.findViewById(R.id.favourite_edit_delete) as ImageView
+    }
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt
new file mode 100644
index 0000000000000000000000000000000000000000..970e2ad54c03a8a60f601be937903c1ed3016f0f
--- /dev/null
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt
@@ -0,0 +1,88 @@
+package ml.adamsprogs.bimba.models
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.util.Log
+import com.google.gson.Gson
+import com.google.gson.JsonArray
+import com.google.gson.JsonObject
+
+class FavouriteStorage(val context: Context) {
+    val favourites = HashMap<String, Favourite>()
+    val preferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE)
+    val favouritesList: List<Favourite>
+        get() {
+            return favourites.values.toList()
+        }
+
+    init {
+        refresh()
+    }
+
+    fun refresh() {
+        val favouritesString = preferences.getString("favourites", "{}")
+        val favouritesMap = Gson().fromJson(favouritesString, JsonObject::class.java)
+        for ((name, jsonTimetables) in favouritesMap.entrySet()) {
+            val timetables = ArrayList<HashMap<String, String>>()
+            for (jsonTimetable in jsonTimetables.asJsonArray) {
+                val timetable = HashMap<String, String>()
+                timetable["stop"] = jsonTimetable.asJsonObject["stop"].asString
+                timetable["line"] = jsonTimetable.asJsonObject["line"].asString
+                timetables.add(timetable)
+            }
+            favourites[name] = Favourite(name, timetables)
+        }
+    }
+
+    fun has(name: String): Boolean = favourites.contains(name)
+
+    fun add(name: String, timetables: ArrayList<HashMap<String, String>>) {
+        if (favourites[name] == null) {
+            favourites[name] = Favourite(name, timetables)
+            serialize()
+        }
+    }
+
+    fun delete(name: String) {
+        favourites.remove(name)
+        serialize()
+    }
+
+    fun delete(name: String, stop: String, line: String) {
+        Log.i("ROW", "delete $name, $stop, $line")
+        favourites[name]?.delete(stop, line)
+        //todo check empty
+        serialize()
+    }
+
+    fun serialize() {
+        val rootObject = JsonObject()
+        for ((name, favourite) in favourites) {
+            val timetables = JsonArray()
+            for (timetable in favourite.timetables) {
+                val element = JsonObject()
+                element.addProperty("stop", timetable["stop"])
+                element.addProperty("line", timetable["line"])
+                timetables.add(element)
+            }
+            rootObject.add(name, timetables)
+        }
+        val favouritesString = Gson().toJson(rootObject)
+        Log.i("FAB", favouritesString)
+        val editor = preferences.edit()
+        editor.putString("favourites", favouritesString)
+        editor.apply()
+    }
+
+    fun detach(name: String, stop: String, line: String, newName: String) {
+        val element = HashMap<String, String>()
+        element["stop"] = stop
+        element["line"] = line
+        val array = ArrayList<HashMap<String, String>>()
+        array.add(element)
+        favourites[newName] = Favourite(newName, array)
+        serialize()
+
+        delete(name, stop, line)
+    }
+}
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt
index d97e13817f530006d3a3d4537557e9bedb0f374a..b0df901aa1f92774af921db31a166d13fbd16508 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt
@@ -12,7 +12,7 @@ import android.view.LayoutInflater
 import java.util.*
 
 
-class FavouritesAdapter(val context: Context, val favourites: List<Favourite>) :
+class FavouritesAdapter(val context: Context, var favourites: List<Favourite>, val onMenuItemClickListener: FavouritesAdapter.OnMenuItemClickListener) :
         RecyclerView.Adapter<FavouritesAdapter.ViewHolder>() {
     override fun getItemCount(): Int {
         return favourites.size
@@ -37,8 +37,8 @@             val popup = PopupMenu(context, it)
             val inflater = popup.menuInflater
             popup.setOnMenuItemClickListener {
                 when (it.itemId) {
-                    R.id.favourite_edit -> editFavourite(favourite.name)
-                    R.id.favourite_delete -> deleteFavourite(favourite.name)
+                    R.id.favourite_edit -> onMenuItemClickListener.edit(favourite.name)
+                    R.id.favourite_delete -> onMenuItemClickListener.delete(favourite.name)
                     else -> false
                 }
             }
@@ -47,16 +47,6 @@             popup.show()
         }
     }
 
-    private fun editFavourite(name: String): Boolean {
-        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
-        return true
-    }
-
-    private fun deleteFavourite(name: String): Boolean {
-        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
-        return true
-    }
-
     override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
         val context = parent?.context
         val inflater = LayoutInflater.from(context)
@@ -71,5 +61,10 @@         val nameTextView = itemView.findViewById(R.id.favourite_name) as TextView
         val timeTextView = itemView.findViewById(R.id.favourite_time) as TextView
         val lineTextView = itemView.findViewById(R.id.favourite_line) as TextView
         val moreButton = itemView.findViewById(R.id.favourite_more_button) as ImageView
+    }
+
+    interface OnMenuItemClickListener {
+        fun edit(name: String): Boolean
+        fun delete(name: String): Boolean
     }
 }
\ No newline at end of file




diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
index 2117cdd3dbd5a679c3f0ffb51d6b55e37a610802..bf8d21faf6cccb40a3bbf0ac8155448e0dbaffd9 100644
--- a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
+++ b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt
@@ -5,18 +5,39 @@ import android.database.Cursor
 import android.database.sqlite.SQLiteCantOpenDatabaseException
 import android.database.sqlite.SQLiteDatabase
 import android.database.sqlite.SQLiteDatabaseCorruptException
-import android.util.Log
 import java.io.File
 
-class Timetable(var context: Context) {
-    var db: SQLiteDatabase? = null
+private var timetable: Timetable? = null
+
+fun getTimetable(context: Context? = null, force: Boolean = false): Timetable {
+    if (timetable == null || force)
+        if (context != null) {
+            val db: SQLiteDatabase?
+            try {
+                db = SQLiteDatabase.openDatabase(File(context.filesDir, "timetable.db").path,
+                        null, SQLiteDatabase.OPEN_READONLY)
+            } catch(e: NoSuchFileException) {
+                throw SQLiteCantOpenDatabaseException("no such file")
+            } catch(e: SQLiteCantOpenDatabaseException) {
+                throw SQLiteCantOpenDatabaseException("cannot open db")
+            } catch(e: SQLiteDatabaseCorruptException) {
+                throw SQLiteCantOpenDatabaseException("db corrupt")
+            }
+            timetable = Timetable()
+            timetable!!.db = db
+            return timetable as Timetable
+        }
+        else
+            throw IllegalArgumentException("new timetable requested and no context given")
+    else
+        return timetable as Timetable
+}
+
+class Timetable internal constructor() {
+    lateinit var db: SQLiteDatabase
 
     init {
         readDbFile()
-    }
-
-    fun isDatabaseHealthy(): Boolean {
-        return db != null
     }
 
     fun refresh() {
@@ -24,29 +45,17 @@         readDbFile()
     }
 
     private fun readDbFile() {
-        try {
-            db = SQLiteDatabase.openDatabase(File(context.filesDir, "timetable.db").path,
-                    null, SQLiteDatabase.OPEN_READONLY)
-        } catch(e: SQLiteCantOpenDatabaseException) {
-            Log.e("Timetable", "Cannot open database")
-            db = null
-        } catch(e: SQLiteDatabaseCorruptException) {
-            Log.e("Timetable", "Database is corrupted")
-            db = null
-        }
     }
 
     fun getStops(): ArrayList<StopSuggestion>? {
-        if (db == null)
-            return null
         val stops = ArrayList<StopSuggestion>()
-        var cursor : Cursor? = null
+        var cursor: Cursor? = null
         try {
-            cursor = db!!.rawQuery("select name ||char(10)|| headsigns as suggestion, id, stops.symbol || number as stopSymbol from stops" +
+            cursor = db.rawQuery("select name ||char(10)|| headsigns as suggestion, id, stops.symbol || number as stopSymbol from stops" +
                     " join nodes on(stops.symbol = nodes.symbol) order by name, id;", null)
             while (cursor.moveToNext())
                 stops.add(StopSuggestion(cursor.getString(0), cursor.getString(1), cursor.getString(2)))
-        }catch (e: SQLiteDatabaseCorruptException) {
+        } catch (e: SQLiteDatabaseCorruptException) {
             cursor?.close()
             return null
         } finally {
@@ -56,9 +65,7 @@         return stops
     }
 
     fun getStopName(stopId: String): String? {
-        if (db == null)
-            return null
-        val cursor = db!!.rawQuery("select name from nodes join stops on(stops.symbol = nodes.symbol) where id = ?;",
+        val cursor = db.rawQuery("select name from nodes join stops on(stops.symbol = nodes.symbol) where id = ?;",
                 listOf(stopId).toTypedArray())
         val name: String
         cursor.moveToNext()
@@ -68,14 +75,12 @@         return name
     }
 
     fun getStopDepartures(stopId: String, lineId: String? = null): HashMap<String, ArrayList<Departure>>? {
-        if (db == null)
-            return null
-        val andLine:String
+        val andLine: String
         if (lineId == null)
             andLine = ""
         else
             andLine = "and line_id = '$lineId'"
-        val cursor = db!!.rawQuery("select lines.number, mode, substr('0'||hour, -2) || ':' || " +
+        val cursor = db.rawQuery("select lines.number, mode, substr('0'||hour, -2) || ':' || " +
                 "substr('0'||minute, -2) as time, lowFloor, modification, headsign from departures join " +
                 "timetables on(timetable_id = timetables.id) join lines on(line_id = lines.id) where " +
                 "stop_id = ? $andLine order by mode, time;", listOf(stopId).toTypedArray())
@@ -93,9 +98,7 @@         return departures
     }
 
     fun getLines(stopId: String?): ArrayList<String>? {
-        if (db == null)
-            return null
-        val cursor = db!!.rawQuery(" select distinct line_id from timetables join " +
+        val cursor = db.rawQuery(" select distinct line_id from timetables join " +
                 "stops on(stop_id = stops.id) where stops.id = ?;",
                 listOf(stopId).toTypedArray())
         val lines = ArrayList<String>()
@@ -106,7 +109,16 @@         cursor.close()
         return lines
     }
 
-    fun close() {
-        db?.close()
+    fun getFavouriteElement(stop: String, line: String): String? {
+        val cursor = db.rawQuery("select name || ' (' || stops.symbol || stops.number || '): \n' " +
+                "|| lines.number || ' → ' || headsign from timetables join stops on (stops.id = stop_id) " +
+                "join lines on(lines.id = line_id) join nodes on(nodes.symbol = stops.symbol) where " +
+                "stop_id = ? and line_id = ?",
+                listOf(stop, line).toTypedArray())
+        val element: String
+        cursor.moveToNext()
+        element = cursor.getString(0)
+        cursor.close()
+        return element
     }
 }




diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 0000000000000000000000000000000000000000..39e64d6980a8c1d7330ec2eb8172779cffd483af
--- /dev/null
+++ b/app/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
+</vector>




diff --git a/app/src/main/res/drawable/ic_split.xml b/app/src/main/res/drawable/ic_split.xml
new file mode 100644
index 0000000000000000000000000000000000000000..327e1d2f554eac9ed106caf94f45a9421e15fd22
--- /dev/null
+++ b/app/src/main/res/drawable/ic_split.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M14,4l2.29,2.29 -2.88,2.88 1.42,1.42 2.88,-2.88L20,10L20,4zM10,4L4,4v6l2.29,-2.29 4.71,4.7L11,20h2v-8.41l-5.29,-5.3z"/>
+</vector>




diff --git a/app/src/main/res/layout/activity_edit_favourite.xml b/app/src/main/res/layout/activity_edit_favourite.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b93a989607cf125b465c9631cbb173bec3502693
--- /dev/null
+++ b/app/src/main/res/layout/activity_edit_favourite.xml
@@ -0,0 +1,65 @@
+<?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"
+    android:id="@+id/dialog_favourite"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <android.support.design.widget.AppBarLayout
+        android:id="@+id/appbar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingTop="@dimen/appbar_padding_top"
+        android:theme="@style/AppTheme.AppBarOverlay">
+
+        <android.support.v7.widget.Toolbar
+            android:id="@+id/toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="?attr/actionBarSize"
+            android:layout_weight="1"
+            android:background="@color/colorPrimary"
+            app:layout_scrollFlags="scroll|enterAlways"
+            app:popupTheme="@style/AppTheme.PopupOverlay" />
+    </android.support.design.widget.AppBarLayout>
+
+    <TextView
+        android:id="@+id/name_label"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="8dp"
+        android:layout_marginTop="8dp"
+        android:labelFor="@id/favourite_name_edit"
+        android:text="@string/favourite_name"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/appbar" />
+
+    <EditText
+        android:id="@+id/favourite_name_edit"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        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" />
+
+    <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"
+        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>
+
+</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
new file mode 100644
index 0000000000000000000000000000000000000000..5128233f1048e216c7a8a55629cd5935e0b31f2f
--- /dev/null
+++ b/app/src/main/res/layout/row_favourite_edit.xml
@@ -0,0 +1,54 @@
+<?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:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+
+    <TextView
+        android:id="@+id/favourite_edit_row"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="16dp"
+        android:layout_marginEnd="16dp"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="16dp"
+        android:gravity="center"
+        android:text=""
+        android:textAlignment="viewStart"
+        android:textAppearance="@style/TextAppearance.AppCompat"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@+id/favourite_edit_split"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <ImageView
+        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_marginTop="16dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toEndOf="@+id/favourite_edit_split"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/ic_delete"
+        tools:layout_editor_absoluteX="335dp"
+        tools:layout_editor_absoluteY="16dp" />
+
+    <ImageView
+        android:id="@+id/favourite_edit_split"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="16dp"
+        android:layout_marginEnd="58dp"
+        android:layout_marginTop="16dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/ic_split"
+        tools:layout_editor_absoluteX="302dp"
+        tools:layout_editor_absoluteY="16dp" />
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 59eab5f8398508474e4978a95a5c2ea60d22b81b..2192e1f29a34a3a9c7a67018d632e24446c9052c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -26,4 +26,8 @@     This stop is already in favourites
     <string name="favourite_row_more_button" translatable="false">favourite row more button</string>
     <string name="action_edit">Edit</string>
     <string name="action_delete">Delete</string>
+    <string name="done">Done</string>
+    <string name="favourite_edit">Edit favourite</string>
+    <string name="favourite_name">Favourite name</string>
+    <string name="edit_favourite_title">Edit ‘%1$s’</string>
 </resources>




diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index d95d96583cbfd31bc307f6f239448f4967e10343..776858ee5161cc4c8e03b8cedf732ab4ed6a4e8a 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -22,4 +22,6 @@     Ten przystanek już jest pośród ulubionych
     <string name="departure_to_line">%1$s → %2$s</string>
     <string name="action_delete">Usuń</string>
     <string name="action_edit">Edytuj</string>
+    <string name="done">Zakończ</string>
+    <string name="favourite_edit">Edytuj ulubiony</string>
 </resources>
\ No newline at end of file