Author: Adam Pioterek <adam.pioterek@protonmail.ch>
Merge branch 'schedulelessness' into develop
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/build.gradle b/app/build.gradle index ceec2719de9cdc6a9df162cb04300385ca2912b3..50d4623bad91c9ccaeac333cac6c4e2a930a7e6e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,9 +4,9 @@ apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 27 - buildToolsVersion "28.0.1" + buildToolsVersion "28.0.2" defaultConfig { - applicationId "ml.adamsprogs.bimba" + applicationId "ml.adamsprogs.bimba.scheduleless" minSdkVersion 19 targetSdkVersion 27 versionCode 14 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5f4681fca49b83a45a0610ee2e176da39900d4ec..58ac9e4d29069e199be5ffbd848d746b75ee26e6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,7 +8,7 @@<application android:allowBackup="true" - android:icon="@mipmap/ic_launcher" + android:icon="@drawable/icon_dev" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> @@ -27,7 +27,6 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> - <activity android:name=".activities.NoDbActivity" /> <activity android:name=".activities.EditFavouriteActivity" /> <activity android:name=".activities.SettingsActivity" @@ -44,7 +43,7 @@ android:label="@string/title_activity_help" android:theme="@style/AppTheme" /> <service - android:name=".datasources.VmClient" + android:name=".datasources.VmService" android:enabled="true" android:exported="false" /> diff --git a/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt b/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt index 9668aa001bce218cf2f16f22b1e70a19b4a8ff75..c40abec7cc2d1edd1e714a8a4f24dbe6afb6d577 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt @@ -4,7 +4,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import ml.adamsprogs.bimba.datasources.TimetableDownloader -import ml.adamsprogs.bimba.datasources.VmClient +import ml.adamsprogs.bimba.datasources.VmService import ml.adamsprogs.bimba.models.Departure import ml.adamsprogs.bimba.models.Plate @@ -28,11 +28,12 @@ for (listener in onTimetableDownloadListeners) { listener.onTimetableDownload(result) } } - if (intent?.action == VmClient.ACTION_READY) { - val departures = intent.getStringArrayListExtra(VmClient.EXTRA_DEPARTURES)?.map { Departure.fromString(it) }?.toSet() - val plateId = intent.getSerializableExtra(VmClient.EXTRA_PLATE_ID) as Plate.ID + if (intent?.action == VmService.ACTION_READY) { + val departures = intent.getStringArrayListExtra(VmService.EXTRA_DEPARTURES)?.map { Departure.fromString(it) }?.toSet() + val plateId = intent.getSerializableExtra(VmService.EXTRA_PLATE_ID) as Plate.ID? + val stopCode = intent.getSerializableExtra(VmService.EXTRA_STOP_CODE) as String for (listener in onVmListeners) { - listener.onVm(departures, plateId) + listener.onVm(departures, plateId, stopCode) } } } @@ -58,6 +59,6 @@ fun onTimetableDownload(result: String?) } interface OnVmListener { - fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID) + fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID?, stopCode: String) } } \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/NetworkStateReceiver.kt b/app/src/main/java/ml/adamsprogs/bimba/NetworkStateReceiver.kt index 70a3a1652bc188fb666fd3841247a52422388b1c..2acd1e1eb9dbb95db986e6907c6e07db549c9792 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/NetworkStateReceiver.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/NetworkStateReceiver.kt @@ -37,9 +37,14 @@ fun onConnectivityChange(connected: Boolean) } companion object { - fun isNetworkAvailable(context: Context): Boolean { - val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val activeNetworkInfo = connectivityManager.activeNetworkInfo + lateinit var manager: ConnectivityManager + + fun init(context: Context) { + manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + } + + fun isNetworkAvailable(): Boolean { + val activeNetworkInfo = manager.activeNetworkInfo return activeNetworkInfo != null && activeNetworkInfo.isConnected } } diff --git a/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt b/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt new file mode 100644 index 0000000000000000000000000000000000000000..32dddcd358b1801b29be9440b9a0c0322cdb8460 --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt @@ -0,0 +1,255 @@ +package ml.adamsprogs.bimba + +import android.content.* +import kotlinx.coroutines.experimental.android.UI +import kotlinx.coroutines.experimental.* +import ml.adamsprogs.bimba.datasources.* +import ml.adamsprogs.bimba.models.* +import ml.adamsprogs.bimba.models.suggestions.* +import java.util.* +import kotlin.collections.HashMap + +//todo make singleton +class ProviderProxy(context: Context? = null) { + private val vmClient = VmClient.getVmClient() + private var timetable: Timetable = Timetable.getTimetable(context) + private var suggestions = emptyList<GtfsSuggestion>() + private val requests = HashMap<String, Request>() + + var mode = if (timetable.isEmpty()) MODE_VM else MODE_FULL + + companion object { + const val MODE_FULL = "mode_full" + const val MODE_VM = "mode_vm" + } + + fun getSuggestions(query: String = "", callback: (List<GtfsSuggestion>) -> Unit) { + launch(UI) { + val filtered = withContext(DefaultDispatcher) { + suggestions = getStopSuggestions(query) //+ getLineSuggestions(query) //todo<p:v+1> + bike stations, train stations, &c + filterSuggestions(query) + } + callback(filtered) + } + } + + private suspend fun getStopSuggestions(query: String): List<StopSuggestion> { + val vmSuggestions = withContext(DefaultDispatcher) { + vmClient.getStops(query) + } + + return if (vmSuggestions.isEmpty() and !timetable.isEmpty()) { + timetable.getStopSuggestions() + } else { + vmSuggestions + } + } + + private fun filterSuggestions(query: String): List<GtfsSuggestion> { + return suggestions.filter { + deAccent(it.name).contains(deAccent(query), true) + } + } + + private fun deAccent(str: String): String { + var result = str.replace('ę', 'e', true) + result = result.replace('ó', 'o', true) + result = result.replace('ą', 'a', true) + result = result.replace('ś', 's', true) + result = result.replace('ł', 'l', true) + result = result.replace('ż', 'z', true) + result = result.replace('ź', 'z', true) + result = result.replace('ć', 'c', true) + result = result.replace('ń', 'n', true) + return result + } + + fun getSheds(name: String, callback: (Map<String, Set<String>>) -> Unit) { + launch(UI) { + val sheds = withContext(DefaultDispatcher) { + val vmSheds = vmClient.getSheds(name) + + if (vmSheds.isEmpty() and !timetable.isEmpty()) { + timetable.getHeadlinesForStop(name) + } else { + vmSheds + } + } + + callback(sheds) + } + } + + fun subscribeForDepartures(stopSegments: Set<StopSegment>, listener: OnDeparturesReadyListener, context: Context): String { + stopSegments.forEach { + val intent = Intent(context, VmService::class.java) + intent.putExtra("stop", it.stop) + intent.action = "request" + context.startService(intent) + } + val uuid = UUID.randomUUID().toString() + requests[uuid] = Request(listener, stopSegments) + return uuid + } + + fun subscribeForDepartures(stopCode: String, listener: OnDeparturesReadyListener, context: Context): String { + val intent = Intent(context, VmService::class.java) + intent.putExtra("stop", stopCode) + intent.action = "request" + context.startService(intent) + + val uuid = UUID.randomUUID().toString() + requests[uuid] = Request(listener, setOf(StopSegment(stopCode, null))) + return uuid + } + + private fun constructSegmentDepartures(stopSegments: Set<StopSegment>): Deferred<Map<String, List<Departure>>> { + return async { + if (timetable.isEmpty()) + emptyMap() + else { + timetable.getStopDeparturesBySegments(stopSegments) + } + } + } + + private fun filterDepartures(departures: Map<String, List<Departure>>): List<Departure> { + val now = Calendar.getInstance().secondsAfterMidnight() + val lines = HashMap<String, Int>() + val twoDayDepartures = (timetable.getServiceForToday()?.let { + departures[it] + } ?: emptyList()) + + (timetable.getServiceForTomorrow()?.let { service -> + departures[service]!!.map { it.copy().apply { tomorrow = true } } + } ?: emptyList()) + + return twoDayDepartures + .filter { it.timeTill(now) >= 0 } + .filter { + val existed = lines[it.line] ?: 0 + if (existed < 3) { + lines[it.line] = existed + 1 + true + } else false + } + } + + fun unsubscribeFromDepartures(uuid: String, context: Context) { + requests[uuid]?.unsubscribe(context) + requests.remove(uuid) + } + + fun refreshTimetable(context: Context) { + timetable = Timetable.getTimetable(context, true) + mode = MODE_FULL + } + + fun getFullTimetable(stopCode: String): Map<String, List<Departure>> { + return if (timetable.isEmpty()) + emptyMap() + else + timetable.getStopDepartures(stopCode) + + } + + fun getFullTimetable(stopSegments: Set<StopSegment>): Map<String, List<Departure>> { + return if (timetable.isEmpty()) + emptyMap() + else + timetable.getStopDeparturesBySegments(stopSegments) + + } + + fun fillStopSegment(stopSegment: StopSegment, callback: (StopSegment?) -> Unit) { + launch(UI) { + withContext(DefaultDispatcher) { + callback(fillStopSegment(stopSegment)) + } + } + } + + suspend fun fillStopSegment(stopSegment: StopSegment): StopSegment? { + if (stopSegment.plates != null) + return stopSegment + + return if (timetable.isEmpty()) + vmClient.getDirections(stopSegment.stop) + else + timetable.getHeadlinesForStopCode(stopSegment.stop) + } + + fun getStopName(stopCode: String, callback: (String?) -> Unit) { + launch(UI) { + withContext(DefaultDispatcher) { + callback(getStopName(stopCode)) + } + } + } + + suspend fun getStopName(stopCode: String): String? { + return if (timetable.isEmpty()) + vmClient.getName(stopCode) + else + timetable.getStopName(stopCode) + } + + fun describeService(service: String, context: Context): String? { + return if (timetable.isEmpty()) + null + else + timetable.getServiceDescription(service, context) + } + + fun getServiceFirstDay(service: String): Int { + return timetable.getServiceFirstDay(service) + } + + interface OnDeparturesReadyListener { + fun onDeparturesReady(departures: List<Departure>, plateId: Plate.ID?) + } + + inner class Request(private val listener: OnDeparturesReadyListener, private val segments: Set<StopSegment>) : MessageReceiver.OnVmListener { + private val receiver = MessageReceiver.getMessageReceiver() + private val receivedPlates = HashSet<Plate.ID>() + + private var cache: Deferred<Map<String, List<Departure>>>? = null + + init { + receiver.addOnVmListener(this@Request) + launch(UI) { + cache = constructSegmentDepartures(segments) + } + } + + override fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID?, stopCode: String) { + launch(UI) { + if (plateId == null) { + listener.onDeparturesReady(filterDepartures(cache!!.await()), null) + } else { + if (segments.any { plateId in it }) { + if (vmDepartures != null) { + listener.onDeparturesReady(vmDepartures.toList(), plateId) + if (plateId !in receivedPlates) + receivedPlates.add(plateId) + } else { + receivedPlates.remove(plateId) + if (receivedPlates.isEmpty()) { + listener.onDeparturesReady(filterDepartures(cache!!.await()), null) + } + } + } + } + } + } + + fun unsubscribe(context: Context) { + segments.forEach { + val intent = Intent(context, VmService::class.java) + intent.putExtra("stop", it.stop) + intent.action = "remove" + context.startService(intent) + } + receiver.removeOnVmListener(this) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt index b3389a44b0e630c67900e4fd810e80964521eae8..a3bcc6e7dc5caf1b31a1d20b76a1062f914329b0 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt @@ -3,8 +3,8 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.* -import android.database.sqlite.SQLiteException import android.os.* +import android.preference.PreferenceManager.getDefaultSharedPreferences import android.support.design.widget.* import android.support.v4.widget.* import android.support.v7.widget.* @@ -12,7 +12,6 @@ import android.support.v7.app.* import android.text.Html import android.view.* import android.view.inputmethod.InputMethodManager -import kotlin.concurrent.thread import kotlin.collections.ArrayList import kotlinx.android.synthetic.main.activity_dash.* import java.util.* @@ -30,11 +29,11 @@ import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion //todo<p:1> searchView integration class DashActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, - FavouritesAdapter.OnMenuItemClickListener, Favourite.OnVmPreparedListener, - FavouritesAdapter.ViewHolder.OnClickListener { + FavouritesAdapter.OnMenuItemClickListener, FavouritesAdapter.ViewHolder.OnClickListener, ProviderProxy.OnDeparturesReadyListener { + val context: Context = this private val receiver = MessageReceiver.getMessageReceiver() - var timetable: Timetable? = null + private lateinit var timetable: Timetable private var suggestions: List<GtfsSuggestion>? = null private lateinit var drawerLayout: DrawerLayout private lateinit var drawerView: NavigationView @@ -45,6 +44,7 @@ private lateinit var adapter: FavouritesAdapter private val actionModeCallback = ActionModeCallback() private var actionMode: ActionMode? = null private var isWarned = false + private lateinit var providerProxy: ProviderProxy companion object { const val REQUEST_EDIT_FAVOURITE = 1 @@ -56,11 +56,9 @@ setContentView(R.layout.activity_dash) setSupportActionBar(toolbar) - timetable = try { - Timetable.getTimetable(this) - } catch (e: SQLiteException) { - null - } + providerProxy = ProviderProxy(this) + timetable = Timetable.getTimetable() + NetworkStateReceiver.init(this) getSuggestions() @@ -77,7 +75,7 @@ //drawer.setCheckedItem(R.id.drawer_home) drawerView.setNavigationItemSelectedListener { item -> when (item.itemId) { R.id.drawer_refresh -> { - startDownloaderService() + startDownloaderService(true) } R.id.drawer_help -> { startActivity(Intent(context, HelpActivity::class.java)) @@ -99,7 +97,9 @@ searchView.setOnFocusChangeListener(object : FloatingSearchView.OnFocusChangeListener { override fun onFocus() { favouritesList.visibility = View.GONE - filterSuggestions(searchView.query) + providerProxy.getSuggestions(searchView.query) { + searchView.swapSuggestions(it) + } } override fun onFocusCleared() { @@ -110,7 +110,9 @@ searchView.setOnQueryChangeListener { oldQuery, newQuery -> if (oldQuery != "" && newQuery == "") searchView.clearSuggestions() - filterSuggestions(newQuery) + providerProxy.getSuggestions(newQuery) { + searchView.swapSuggestions(it) + } } searchView.setOnSearchListener(object : FloatingSearchView.OnSearchListener { @@ -123,7 +125,6 @@ } imm.hideSoftInputFromWindow(view.windowToken, 0) if (searchSuggestion is StopSuggestion) { val intent = Intent(context, StopSpecifyActivity::class.java) - intent.putExtra(StopSpecifyActivity.EXTRA_STOP_IDS, searchSuggestion.ids.joinToString(",") { it.id }) intent.putExtra(StopSpecifyActivity.EXTRA_STOP_NAME, searchSuggestion.name) startActivity(intent) } else if (searchSuggestion is LineSuggestion) { @@ -151,41 +152,49 @@ searchView.attachNavigationDrawerToMenuButton(drawer_layout as DrawerLayout) } + override fun onRestart() { + super.onRestart() + favourites = FavouriteStorage.getFavouriteStorage(context) + favourites.forEach { + it.subscribeForDepartures(this, this) + } + } + + override fun onStop() { + super.onStop() + favourites.forEach { + it.unsubscribeFromDepartures(this) + } + } + private fun showValidityInDrawer() { - if (timetable == null) { + if (timetable.isEmpty()) { drawerView.menu.findItem(R.id.drawer_validity_since).title = getString(R.string.validity_offline_unavailable) } else { val formatter = DateFormat.getDateInstance(DateFormat.SHORT) - var calendar = calendarFromIsoD(timetable!!.getValidSince()) + var calendar = calendarFromIsoD(timetable.getValidSince()) formatter.timeZone = calendar.timeZone drawerView.menu.findItem(R.id.drawer_validity_since).title = getString(R.string.valid_since, formatter.format(calendar.time)) - calendar = calendarFromIsoD(timetable!!.getValidTill()) + calendar = calendarFromIsoD(timetable.getValidTill()) formatter.timeZone = calendar.timeZone drawerView.menu.findItem(R.id.drawer_validity_till).title = getString(R.string.valid_till, formatter.format(calendar.time)) } } - private fun filterSuggestions(newQuery: String) { - thread { - val newStops = suggestions!!.filter { deAccent(it.name).contains(deAccent(newQuery), true) } //todo<p:2> sorted by similarity - runOnUiThread { searchView.swapSuggestions(newStops) } - } - } - private fun warnTimetableValidity() { if (isWarned) return isWarned = true - if (timetable == null) + if (timetable.isEmpty()) return - val validTill = timetable!!.getValidTill() + val validTill = timetable.getValidTill() val today = Calendar.getInstance().toIsoDate() val tomorrow = Calendar.getInstance().apply { this.add(Calendar.DAY_OF_MONTH, 1) }.toIsoDate() try { - timetable!!.getServiceForToday() + timetable.getServiceForToday() if (today > validTill) { notifyTimetableValidity(-1) suggestions = ArrayList() @@ -202,7 +211,7 @@ return } try { - timetable!!.getServiceForTomorrow() + timetable.getServiceForTomorrow() if (tomorrow == validTill) { notifyTimetableValidity(1) return @@ -226,12 +235,14 @@ ) { dialog: DialogInterface, _: Int -> dialog.cancel() } .setCancelable(true) .setMessage(message) .create().show() + + //todo if days == -1 -> delete timetable } private fun prepareFavourites() { favourites = FavouriteStorage.getFavouriteStorage(context) favourites.forEach { - it.addOnVmPreparedListener(this) + it.subscribeForDepartures(this, this) } val layoutManager = LinearLayoutManager(context) favouritesList = favourites_list @@ -241,31 +252,30 @@ favouritesList.itemAnimator = DefaultItemAnimator() favouritesList.layoutManager = layoutManager } - override fun onVmPrepared() { + override fun onDeparturesReady(departures: List<Departure>, plateId: Plate.ID?) { favouritesList.adapter.notifyDataSetChanged() } private fun getSuggestions() { - suggestions = if (timetable != null) - (timetable!!.getStopSuggestions(context)).sorted() //+ timetable.getLineSuggestions()).sorted() //todo<p:v+1> + bike stations, train stations, &c - else - emptyList() + providerProxy.getSuggestions { + searchView.swapSuggestions(it) + } } private fun prepareListeners() { val filter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED) - filter.addAction(VmClient.ACTION_READY) + filter.addAction(VmService.ACTION_READY) filter.addCategory(Intent.CATEGORY_DEFAULT) registerReceiver(receiver, filter) receiver.addOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener) - favourites.registerOnVm(receiver, context) } - private fun startDownloaderService() { - startService(Intent(context, TimetableDownloader::class.java)) + private fun startDownloaderService(force: Boolean = false) { + if (getDefaultSharedPreferences(this).getBoolean("automatic timetable updates", false) or force) + startService(Intent(context, TimetableDownloader::class.java)) } - override fun onBackPressed() { + override fun onBackPressed() { //fixme if (drawerLayout.isDrawerOpen(drawerView)) { drawerLayout.closeDrawer(drawerView) return @@ -284,21 +294,7 @@ override fun onDestroy() { super.onDestroy() receiver.removeOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener) - favourites.deregisterOnVm(receiver, context) unregisterReceiver(receiver) - } - - private fun deAccent(str: String): String { - var result = str.replace('ę', 'e', true) - result = result.replace('ó', 'o', true) - result = result.replace('ą', 'a', true) - result = result.replace('ś', 's', true) - result = result.replace('ł', 'l', true) - result = result.replace('ż', 'z', true) - result = result.replace('ź', 'z', true) - result = result.replace('ć', 'c', true) - result = result.replace('ń', 'n', true) - return result } override fun onTimetableDownload(result: String?) { diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/LineSpecifyActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/LineSpecifyActivity.kt index 5d7d949f83b9112fd606741d84d156d57990cd8a..e33d1edd5cf5253071bf0936ca77d35211ce2426 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/LineSpecifyActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/LineSpecifyActivity.kt @@ -30,7 +30,7 @@ val line = intent.getStringExtra(EXTRA_LINE_ID) val timetable = Timetable.getTimetable() - val graphs = timetable.getTripGraphs(AgencyAndId(line)) + val graphs = timetable.getTripGraphs(line) sectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager, graphs) diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt deleted file mode 100644 index 12157af53cd4f527780038c4c7495da4cb8c1c92..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt +++ /dev/null @@ -1,105 +0,0 @@ -package ml.adamsprogs.bimba.activities - -import android.content.Context -import android.content.Intent -import android.support.v7.app.AppCompatActivity -import android.os.Bundle -import android.content.IntentFilter -import ml.adamsprogs.bimba.* -import kotlinx.android.synthetic.main.activity_nodb.* -import ml.adamsprogs.bimba.datasources.TimetableDownloader -import ml.adamsprogs.bimba.models.Timetable - -class NoDbActivity : AppCompatActivity(), NetworkStateReceiver.OnConnectivityChangeListener, MessageReceiver.OnTimetableDownloadListener { - private val networkStateReceiver = NetworkStateReceiver() - private val timetableDownloadReceiver = MessageReceiver.getMessageReceiver() - private var serviceRunning = false - private var askedForNetwork = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_nodb) - val editor = getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE).edit() - editor.putString("etag", "") - editor.apply() - - var filter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED) - filter.addCategory(Intent.CATEGORY_DEFAULT) - registerReceiver(timetableDownloadReceiver, filter) - timetableDownloadReceiver.addOnTimetableDownloadListener(this) - - if (!NetworkStateReceiver.isNetworkAvailable(this)) { - askedForNetwork = true - no_db_caption.text = getString(R.string.no_db_connect) - filter = IntentFilter("android.net.conn.CONNECTIVITY_CHANGE") - registerReceiver(networkStateReceiver, filter) - networkStateReceiver.addOnConnectivityChangeListener(this) - } else - downloadTimetable() - - skip_button.setOnClickListener { - /* - val editor = getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE).edit() - editor.putBoolean(Timetable.ONLY_ONLINE, true) - editor.apply()*/ - startActivity(Intent(this, DashActivity::class.java)) - finish() - } - } - - override fun onResume() { - super.onResume() - try { - val timetable = Timetable.getTimetable(this, true) - if (!timetable.isEmpty()) { - startActivity(Intent(this, DashActivity::class.java)) - finish() - } - } catch (e:Exception){} - var filter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED) - filter.addCategory(Intent.CATEGORY_DEFAULT) - registerReceiver(timetableDownloadReceiver, filter) - if (!NetworkStateReceiver.isNetworkAvailable(this)) { - askedForNetwork = true - no_db_caption.text = getString(R.string.no_db_connect) - filter = IntentFilter("android.net.conn.CONNECTIVITY_CHANGE") - registerReceiver(networkStateReceiver, filter) - networkStateReceiver.addOnConnectivityChangeListener(this) - } else if (!serviceRunning) - downloadTimetable() - } - - private fun downloadTimetable() { - no_db_caption.text = getString(R.string.no_db_downloading) - serviceRunning = true - intent = Intent(this, TimetableDownloader::class.java) - intent.putExtra(TimetableDownloader.EXTRA_FORCE, true) - startService(intent) - } - - override fun onConnectivityChange(connected: Boolean) { - if (connected && !serviceRunning) - downloadTimetable() - /*if (!connected) - serviceRunning = false*/ - } - - override fun onTimetableDownload(result: String?) { - when (result) { - TimetableDownloader.RESULT_FINISHED -> { - timetableDownloadReceiver.removeOnTimetableDownloadListener(this) - networkStateReceiver.removeOnConnectivityChangeListener(this) - startActivity(Intent(this, DashActivity::class.java)) - finish() - } - else -> no_db_caption.text = getString(R.string.error_try_later) - } - } - - override fun onPause() { - super.onPause() - unregisterReceiver(timetableDownloadReceiver) - if (askedForNetwork) - unregisterReceiver(networkStateReceiver) - } -} diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt index 5a2d486aa09d33eae44c5d3d1686830a8adcbb2d..e7c3e0085a8ad06f180703cd533e0848579b9752 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/SplashActivity.kt @@ -14,15 +14,7 @@ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO) - try { - val timetable = Timetable.getTimetable(this) - if (timetable.isEmpty()) - startActivity(Intent(this, NoDbActivity::class.java)) - else - startActivity(Intent(this, DashActivity::class.java)) - } catch (e:Exception) { - startActivity(Intent(this, NoDbActivity::class.java)) - } + startActivity(Intent(this, DashActivity::class.java)) finish() } } diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt index a369cc81ddece00c5313ef0e19482d13785f74a1..b6dbb3eeab4dbf3ca1b8ef2bd3077c7276377fd9 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt @@ -4,27 +4,24 @@ import android.content.* import android.support.design.widget.* import android.os.Bundle import android.view.* -import android.support.v4.app.* -import android.support.v4.view.PagerAdapter import android.support.v4.content.res.ResourcesCompat import android.support.v7.app.AppCompatActivity import android.support.v7.widget.* +import android.widget.AdapterView import java.util.Calendar import kotlinx.android.synthetic.main.activity_stop.* import ml.adamsprogs.bimba.* import ml.adamsprogs.bimba.collections.FavouriteStorage import ml.adamsprogs.bimba.datasources.* -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId import ml.adamsprogs.bimba.models.* import ml.adamsprogs.bimba.models.adapters.DeparturesAdapter - -class StopActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, MessageReceiver.OnVmListener, Favourite.OnVmPreparedListener { +import ml.adamsprogs.bimba.models.adapters.ServiceAdapter - private var sectionsPagerAdapter: SectionsPagerAdapter? = null - +class StopActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, ProviderProxy.OnDeparturesReadyListener { companion object { - const val EXTRA_STOP_ID = "stopId" + const val EXTRA_STOP_CODE = "stopCode" + const val EXTRA_STOP_NAME = "stopName" const val EXTRA_FAVOURITE = "favourite" const val SOURCE_TYPE = "sourceType" const val SOURCE_TYPE_STOP = "stop" @@ -33,108 +30,79 @@ const val MODE_WORKDAYS = 0 const val MODE_SATURDAYS = 1 const val MODE_SUNDAYS = 2 + + const val TIMETABLE_TYPE_DEPARTURE = "timetable_type_departure" + const val TIMETABLE_TYPE_FULL = "timetable_type_full" } - private var stopSegment: StopSegment? = null + private var stopCode = "" private var favourite: Favourite? = null - private var timetableType = "departure" - private lateinit var timetable: Timetable + private var timetableType = TIMETABLE_TYPE_DEPARTURE private val context = this private val receiver = MessageReceiver.getMessageReceiver() - private val vmDepartures = HashMap<Plate.ID, Set<Departure>>() - private var hasDepartures = false - private var lastUpdated = 0L + private lateinit var providerProxy: ProviderProxy + private val departures = HashMap<Plate.ID, List<Departure>>() + private val fullDepartures = HashMap<String, List<Departure>>() + private lateinit var subscriptionId: String + private lateinit var adapter: DeparturesAdapter + private lateinit var sourceType: String override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_stop) - timetable = Timetable.getTimetable(this) + providerProxy = ProviderProxy(this) sourceType = intent.getStringExtra(SOURCE_TYPE) setSupportActionBar(toolbar) - val departures = when (sourceType) { + when (sourceType) { SOURCE_TYPE_STOP -> { - stopSegment = StopSegment(intent.getSerializableExtra(EXTRA_STOP_ID) as AgencyAndId, null).apply { fillPlates() } - supportActionBar?.title = timetable.getStopName(stopSegment!!.stop) - null + stopCode = intent.getSerializableExtra(EXTRA_STOP_CODE) as String + supportActionBar?.title = intent.getSerializableExtra(EXTRA_STOP_NAME) as String } SOURCE_TYPE_FAV -> { favourite = intent.getParcelableExtra(EXTRA_FAVOURITE) supportActionBar?.title = favourite!!.name - favourite!!.addOnVmPreparedListener(this) - if (favourite!!.fullDepartures.isNotEmpty()) - favourite!!.fullDepartures - else - null } - else -> null } showFab() - sectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager, departures) - - container.adapter = sectionsPagerAdapter - - container.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs)) - tabs.addOnTabSelectedListener(TabLayout.ViewPagerOnTabSelectedListener(container)) + val layoutManager = LinearLayoutManager(this) + departuresList.addItemDecoration(DividerItemDecoration(departuresList.context, layoutManager.orientation)) + departuresList.adapter = DeparturesAdapter(this, null, true) + adapter = departuresList.adapter as DeparturesAdapter + departuresList.layoutManager = layoutManager - selectTodayPage() + departuresList.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {} + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + updateFabVisibility(dy) + super.onScrolled(recyclerView, dx, dy) + } + }) prepareOnDownloadListener() - } - - private fun getFavouriteDepartures() { - refreshAdapter(favourite!!.allDepartures()) - } - - private fun refreshAdapterFromStop() { - val now = Calendar.getInstance().secondsAfterMidnight() - val departures = HashMap<AgencyAndId, List<Departure>>() - if (this.vmDepartures.isNotEmpty()) { - departures[timetable.getServiceForToday()] = this.vmDepartures.flatMap { it.value }.sortedBy { it.timeTill(now) } - refreshAdapter(departures) - } else { - refreshAdapter(Departure.createDepartures(stopSegment!!.stop)) - hasDepartures = true - } - } - - private fun refreshAdapter(departures: Map<AgencyAndId, List<Departure>>?) { - if (departures != null) - sectionsPagerAdapter?.departures = departures - sectionsPagerAdapter?.notifyDataSetChanged() - selectTodayPage() - lastUpdated = Calendar.getInstance().timeInMillis - } - - override fun onVmPrepared() { - // println("onVmPrepared: ticked? ${ticked()}; vmBacked? ${favourite!!.isBackedByVm}") - if ((favourite!!.isBackedByVm || ticked()) && (timetableType == "departure")) { - getFavouriteDepartures() - } + subscribeForDepartures() } private fun showFab() { if (sourceType == SOURCE_TYPE_FAV) return - val stopSymbol = timetable.getStopCode(stopSegment!!.stop) - val favourites = FavouriteStorage.getFavouriteStorage(context) - if (!favourites.has(stopSymbol)) { + if (!favourites.has(stopCode)) { fab.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_favourite_empty, this.theme)) } fab.setOnClickListener { - if (!favourites.has(stopSymbol)) { + if (!favourites.has(stopCode)) { val items = HashSet<StopSegment>() - items.add(stopSegment!!) - favourites.add(stopSymbol, items, this@StopActivity) + items.add(StopSegment(stopCode, null)) + favourites.add(stopCode, items, this@StopActivity) fab.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_favourite, this.theme)) } else { Snackbar.make(it, getString(R.string.stop_already_fav), Snackbar.LENGTH_LONG) @@ -156,41 +124,43 @@ } private fun prepareOnDownloadListener() { val filter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED) - filter.addAction(VmClient.ACTION_READY) + filter.addAction(VmService.ACTION_READY) filter.addCategory(Intent.CATEGORY_DEFAULT) registerReceiver(receiver, filter) receiver.addOnTimetableDownloadListener(context) - if (sourceType == SOURCE_TYPE_STOP) { - receiver.addOnVmListener(context) - val intent = Intent(this, VmClient::class.java) - intent.putExtra("stop", stopSegment) - intent.action = "request" - startService(intent) + } + + private fun subscribeForDepartures() { + subscriptionId = if (sourceType == SOURCE_TYPE_STOP) { + providerProxy.subscribeForDepartures(stopCode, this, this) } else - favourite!!.registerOnVm(receiver, context) + favourite!!.subscribeForDepartures(this, context) } - override fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID) { - // println("onVm") - if (vmDepartures == null && this.vmDepartures.isEmpty() && hasDepartures) { - // println("\tbut noVM") - if (ticked()) { - // println("\t\tbut ticked") - refreshAdapterFromStop() - } - return - } - if (timetableType == "departure" && stopSegment!!.contains(plateId)) { - // println("\tthere’s still vm") - if (vmDepartures != null) - this.vmDepartures[plateId] = vmDepartures - else - this.vmDepartures.remove(plateId) - refreshAdapterFromStop() + override fun onDeparturesReady(departures: List<Departure>, plateId: Plate.ID?) { + if (plateId == null) { + this.departures.clear() + this.departures[Plate.ID.dummy] = departures + } else { + this.departures.remove(Plate.ID.dummy) + this.departures[plateId] = departures } + if (timetableType == TIMETABLE_TYPE_FULL) + return + refreshAdapter() } - private fun ticked() = Calendar.getInstance().timeInMillis - lastUpdated >= VmClient.TICK_6_ZINA_TIM_WITH_MARGIN + private fun refreshAdapter() { + if (timetableType == TIMETABLE_TYPE_FULL) { + @Suppress("UNCHECKED_CAST") + adapter.departures = fullDepartures[(dateSpinner.selectedItem as ServiceAdapter.RowItem).service] + } else { + val now = Calendar.getInstance() + val seconds = now.secondsAfterMidnight() + adapter.departures = this.departures.flatMap { it.value }.sortedBy { it.timeTill(seconds) } + } + adapter.notifyDataSetChanged() + } override fun onTimetableDownload(result: String?) { val message: String = when (result) { @@ -203,17 +173,12 @@ try { Snackbar.make(findViewById(R.id.stop_layout), message, Snackbar.LENGTH_LONG).show() } catch (e: IllegalArgumentException) { } - timetable = Timetable.getTimetable(this, true) - if (sourceType == SOURCE_TYPE_STOP) - refreshAdapterFromStop() - } - - private fun selectTodayPage() { - tabs.getTabAt(sectionsPagerAdapter!!.todayTab())!!.select() + providerProxy.refreshTimetable(this) } override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_stop, menu) + if (providerProxy.mode == ProviderProxy.MODE_FULL) + menuInflater.inflate(R.menu.menu_stop, menu) return true } @@ -221,22 +186,42 @@ override fun onOptionsItemSelected(item: MenuItem): Boolean { val id = item.itemId if (id == R.id.action_change_type) { - if (timetableType == "departure") { - timetableType = "full" + if (timetableType == TIMETABLE_TYPE_DEPARTURE) { + timetableType = TIMETABLE_TYPE_FULL + item.icon = (ResourcesCompat.getDrawable(resources, R.drawable.ic_timetable_departure, this.theme)) - sectionsPagerAdapter?.relativeTime = false - if (sourceType == SOURCE_TYPE_STOP) - refreshAdapter(timetable.getStopDepartures(stopSegment!!.stop)) - else - refreshAdapter(favourite!!.fullTimetable()) + adapter.relativeTime = false + if (fullDepartures.isEmpty()) + if (sourceType == SOURCE_TYPE_STOP) + fullDepartures.putAll(providerProxy.getFullTimetable(stopCode)) + else + fullDepartures.putAll(favourite!!.fullTimetable()) + + dateSpinner.let { spinner -> + spinner.adapter = ServiceAdapter(this, R.layout.toolbar_spinner_item, fullDepartures.keys.map { + ServiceAdapter.RowItem(it, providerProxy.describeService(it, this)!!) + }.sorted()).apply { + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + } + spinner.visibility = View.VISIBLE + spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + refreshAdapter() + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + } + + } + } + + refreshAdapter() } else { - timetableType = "departure" + dateSpinner.visibility = View.GONE + timetableType = TIMETABLE_TYPE_DEPARTURE item.icon = (ResourcesCompat.getDrawable(resources, R.drawable.ic_timetable_full, this.theme)) - sectionsPagerAdapter?.relativeTime = true - if (sourceType == SOURCE_TYPE_STOP) - refreshAdapterFromStop() - else - refreshAdapter(favourite!!.allDepartures()) + adapter.relativeTime = true + refreshAdapter() } return true } @@ -247,104 +232,10 @@ override fun onDestroy() { super.onDestroy() receiver.removeOnTimetableDownloadListener(context) - if (sourceType == SOURCE_TYPE_STOP) { - receiver.removeOnVmListener(context) - val intent = Intent(this, VmClient::class.java) - intent.putExtra("stop", stopSegment) - intent.action = "remove" - startService(intent) - } else - favourite!!.deregisterOnVm(receiver, context) + if (sourceType == SOURCE_TYPE_STOP) + providerProxy.unsubscribeFromDepartures(subscriptionId, this) + else + favourite!!.unsubscribeFromDepartures(this) unregisterReceiver(receiver) - } - - inner class SectionsPagerAdapter(fm: FragmentManager, var departures: Map<AgencyAndId, List<Departure>>?) : FragmentStatePagerAdapter(fm) { - var relativeTime = true - - override fun getItem(position: Int): Fragment { - if (departures == null) - return PlaceholderFragment.newInstance(null, relativeTime) { updateFabVisibility(it) } - if (departures!!.isEmpty()) - return PlaceholderFragment.newInstance(ArrayList(), relativeTime) { updateFabVisibility(it) } - val sat = try { - timetable.getServiceFor(Calendar.SATURDAY) - } catch (e: IllegalArgumentException) { - null - } - val sun = try { - timetable.getServiceFor(Calendar.SUNDAY) - } catch (e: IllegalArgumentException) { - null - } - val list: List<Departure> = when (position) { - 1 -> departures!![sat] ?: ArrayList() - 2 -> departures!![sun] ?: ArrayList() - 0 -> try { - departures!! - .filter { it.key != sat && it.key != sun } - .toList()[0].second - } catch (e: IndexOutOfBoundsException) { - ArrayList<Departure>() - } - else -> throw IndexOutOfBoundsException("No tab at index $position") - } - return PlaceholderFragment.newInstance(list, relativeTime) { updateFabVisibility(it) } - } - - override fun getCount() = 3 - - override fun getItemPosition(obj: Any): Int { - return PagerAdapter.POSITION_NONE - } - - fun todayTab(): Int { - return Calendar.getInstance().getMode() - } - } - - class PlaceholderFragment: Fragment() { - lateinit var updater: (Int) -> Unit - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val rootView = inflater.inflate(R.layout.fragment_stop, container, false) - - val layoutManager = LinearLayoutManager(activity) - val departuresList: RecyclerView = rootView.findViewById(R.id.departuresList) - val departures = arguments?.getStringArrayList("departures")?.map { Departure.fromString(it) } - if (departures != null && departures.isNotEmpty()) - departuresList.addItemDecoration(DividerItemDecoration(departuresList.context, layoutManager.orientation)) - - - departuresList.adapter = DeparturesAdapter(activity as Context, departures, - arguments?.get("relativeTime") as Boolean) - departuresList.layoutManager = layoutManager - - departuresList.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {} - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - updater(dy) - super.onScrolled(recyclerView, dx, dy) - } - }) - return rootView - } - - companion object { - fun newInstance(departures: List<Departure>?, relativeTime: Boolean, updater: (Int) -> Unit): PlaceholderFragment { - val fragment = PlaceholderFragment() - fragment.updater = updater - val args = Bundle() - if (departures != null) { - if (departures.isNotEmpty()) { - val d = ArrayList<String>() - departures.mapTo(d) { it.toString() } - args.putStringArrayList("departures", d) - } else - args.putStringArrayList("departures", ArrayList<String>()) - } - args.putBoolean("relativeTime", relativeTime) - fragment.arguments = args - return fragment - } - } } } diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt index eacf1d6d40244cf2c40982f9514211603d2c683f..a81d3536efc67b4b34a1b4b77337be6ba2379288 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt @@ -7,18 +7,16 @@ import android.view.View import android.view.ViewGroup import kotlinx.android.synthetic.main.activity_stop_specify.* import ml.adamsprogs.bimba.R -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId -import ml.adamsprogs.bimba.models.Timetable import android.content.Context import android.widget.TextView import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.RecyclerView import android.view.LayoutInflater +import ml.adamsprogs.bimba.ProviderProxy class StopSpecifyActivity : AppCompatActivity() { companion object { - const val EXTRA_STOP_IDS = "stopIds" const val EXTRA_STOP_NAME = "stopName" } @@ -26,22 +24,24 @@ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_stop_specify) - val ids = intent.getStringExtra(EXTRA_STOP_IDS).split(",").map { AgencyAndId(it) }.toSet() val name = intent.getStringExtra(EXTRA_STOP_NAME) - val timetable = Timetable.getTimetable(this) - val headlines = timetable.getHeadlinesForStop(ids) + val providerProxy = ProviderProxy(this) + providerProxy.getSheds(name) { + val layoutManager = LinearLayoutManager(this) + val departuresList: RecyclerView = list_view - val layoutManager = LinearLayoutManager(this) - val departuresList: RecyclerView = list_view + departuresList.adapter = ShedAdapter(this, it, name) + departuresList.layoutManager = layoutManager + } + /*val timetable = Timetable.getTimetable(this) + val headlines = timetable.getHeadlinesForStop(name)*/ - departuresList.adapter = ShedAdapter(this, headlines) - departuresList.layoutManager = layoutManager setSupportActionBar(toolbar) supportActionBar?.title = name } - class ShedAdapter(val context: Context, private val values: Map<AgencyAndId, Pair<String, Set<String>>>) : + class ShedAdapter(val context: Context, private val values: Map<String, Set<String>>, private val stopName: String) : RecyclerView.Adapter<ShedAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val context = parent.context @@ -55,15 +55,16 @@ override fun getItemCount(): Int = values.size override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.root.setOnClickListener { - val id = values.entries.sortedBy { it.value.first }[position].key + val code = values.keys.sorted()[position] val intent = Intent(context, StopActivity::class.java) intent.putExtra(StopActivity.SOURCE_TYPE, StopActivity.SOURCE_TYPE_STOP) - intent.putExtra(StopActivity.EXTRA_STOP_ID, id) + intent.putExtra(StopActivity.EXTRA_STOP_CODE, code) + intent.putExtra(StopActivity.EXTRA_STOP_NAME, stopName) context.startActivity(intent) } - holder.stopCode.text = values.values.sortedBy { it.first }[position].first - holder.stopHeadlines.text = values.values.sortedBy { it.first }[position].second - .sortedBy { it } // fixme<p:1> natural sort + holder.stopCode.text = values.keys.sorted()[position] + holder.stopHeadlines.text = values.entries.sortedBy { it.key }[position].value + .sortedBy { it.split(" → ")[0].toInt() } .joinToString() } diff --git a/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt b/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt index 2c8e92efd8f69559ae0ec5cc2eadda253d49c80f..d937e039aa9cc926d197f7bebe913883845281a5 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt @@ -4,7 +4,6 @@ import android.content.* import com.google.gson.* import ml.adamsprogs.bimba.* import ml.adamsprogs.bimba.models.* -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId import java.util.Calendar @@ -34,12 +33,19 @@ val favouritesMap = Gson().fromJson(favouritesString, JsonObject::class.java) for ((name, jsonTimetables) in favouritesMap.entrySet()) { val timetables = HashSet<StopSegment>() jsonTimetables.asJsonArray.mapTo(timetables) { - val stopSegment = StopSegment(AgencyAndId(it.asJsonObject["stop"].asString), null) - val plates = HashSet<Plate.ID>() - it.asJsonObject["plates"].asJsonArray.mapTo(plates) { - Plate.ID(AgencyAndId(it.asJsonObject["line"].asString), - AgencyAndId(it.asJsonObject["stop"].asString), - it.asJsonObject["headsign"].asString) + val stopSegment = StopSegment(it.asJsonObject["stop"].asString, null) + val plates = it.asJsonObject["plates"].let { jsonPlates -> + if (jsonPlates == null || jsonPlates.isJsonNull) + null + else { + HashSet<Plate.ID>().apply { + jsonPlates.asJsonArray.map { + Plate.ID(it.asJsonObject["line"].asString, + it.asJsonObject["stop"].asString, + it.asJsonObject["headsign"].asString) + } + } + } } stopSegment.plates = plates stopSegment @@ -79,9 +85,11 @@ positionIndex.remove(name) serialize() } - fun delete(name: String, plate: Plate.ID) { - favourites[name]?.delete(plate) - serialize() + fun delete(name: String, plate: Plate.ID): Boolean { + return favourites[name]?.delete(plate).let { + serialize() + it + } ?: false } private fun serialize() { @@ -90,15 +98,20 @@ for ((name, favourite) in favourites) { val timetables = JsonArray() for (timetable in favourite.segments) { val segment = JsonObject() - segment.addProperty("stop", timetable.stop.id) - val plates = JsonArray() - for (plate in timetable.plates ?: HashSet()) { - val element = JsonObject() - element.addProperty("stop", plate.stop.id) - element.addProperty("line", plate.line.id) - element.addProperty("headsign", plate.headsign) - plates.add(element) - } + segment.addProperty("stop", timetable.stop) + val plates = + if (timetable.plates == null) + JsonNull.INSTANCE + else + JsonArray().apply { + for (plate in timetable.plates ?: HashSet()) { + val element = JsonObject() + element.addProperty("stop", plate.stop) + element.addProperty("line", plate.line) + element.addProperty("headsign", plate.headsign) + add(element) + } + } segment.add("plates", plates) timetables.add(segment) } @@ -115,9 +128,9 @@ fun merge(names: List , context: Context) { if (names.size < 2) return - val newCache = HashMap<AgencyAndId, ArrayList<Departure>>() + val newCache = HashMap<String, ArrayList<Departure>>() names.forEach { - favourites[it]!!.fullDepartures.forEach { + favourites[it]!!.fullTimetable().forEach { if (newCache[it.key] == null) newCache[it.key] = ArrayList() newCache[it.key]!!.addAll(it.value) @@ -147,18 +160,6 @@ positionIndex.remove(oldName) favourites[newName] = favourite addIndex(newName) serialize() - } - - fun registerOnVm(receiver: MessageReceiver, context: Context) { - favourites.values.forEach { - it.registerOnVm(receiver, context) - } - } - - fun deregisterOnVm(receiver: MessageReceiver, context: Context) { - favourites.values.forEach { - it.deregisterOnVm(receiver, context) - } } operator fun get(name: String): Favourite? { diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/CacheManager.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/CacheManager.kt deleted file mode 100644 index 71130764f796ebe17e68897bd6ec9899accfe5b1..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/datasources/CacheManager.kt +++ /dev/null @@ -1,136 +0,0 @@ -package ml.adamsprogs.bimba.datasources - -import android.content.Context -import android.content.SharedPreferences -import ml.adamsprogs.bimba.models.Plate -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId - -class CacheManager private constructor(context: Context) { - companion object { - private var manager: CacheManager? = null - fun getCacheManager(context: Context): CacheManager { - return if (manager == null) { - manager = CacheManager(context) - manager!! - } else - manager!! - } - - val MAX_SIZE = 40 - } - - private var cachePreferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.cachePreferences.cache", Context.MODE_PRIVATE) - private var cacheHitsPreferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.cachePreferences.cacheHits", Context.MODE_PRIVATE) - - private var cache: HashMap<String, Plate> = HashMap() - private var cacheHits: HashMap<String, Int> = HashMap() - - fun keys(): List<Plate> { - return cache.map { - Plate(Plate.ID( - AgencyAndId.convertFromString(it.key.split("@")[0]), - AgencyAndId.convertFromString(it.key.split("@")[1].split(">")[0]), - it.key.split(">")[1]), - null) - } - } - - fun hasAll(plates: HashSet<Plate>): Boolean { - plates - .filterNot { has(it) } - .forEach { return false } - return true - } - - fun hasAny(plates: HashSet<Plate>): Boolean { - plates - .filter { has(it) } - .forEach { return true } - return false - } - - fun has(plate: Plate): Boolean { - return cache.containsKey(key(plate)) - } - - fun push(plates: HashSet<Plate>) { - val removeNumber = cache.size + plates.size - MAX_SIZE - val editor = cachePreferences.edit() - val editorCacheHits = cacheHitsPreferences.edit() - cacheHits.map { "${it.value}|${it.key}" }.sortedBy { it }.slice(0 until removeNumber).forEach { - val key = it.split("|")[1] - cache.remove(key) - editor.remove(key) - } - for (plate in plates) { - val key = key(plate) - cache[key] = plate - cacheHits[key] = 0 - editor.putString(key, cache[key].toString()) - editorCacheHits.putInt(key, 0) - } - editor.apply() - editorCacheHits.apply() - } - - fun push(plate: Plate) { - val editorCache = cachePreferences.edit() - val editorCacheHits = cacheHitsPreferences.edit() - if (cacheHits.size == MAX_SIZE) { - val key = cacheHits.minBy { it.value }?.key - cache.remove(key) - editorCache.remove(key) - cacheHits.remove(key) - editorCacheHits.remove(key) - } - val key = key(plate) - cache[key] = plate - cacheHits[key] = 0 - editorCache.putString(key, plate.toString()) - editorCacheHits.putInt(key, 0) - editorCache.apply() - editorCacheHits.apply() - } - - fun get(plates: HashSet<Plate>): HashSet<Plate> { - val result = HashSet<Plate>() - for (plate in plates) { - val value = get(plate) - if (value == null) - result.add(plate) - else - result.add(value) - } - return result - } - - fun get(plate: Plate): Plate? { - if (!has(plate)) - return null - val key = key(plate) - val hits = cacheHits[key] - if (hits != null) - cacheHits[key] = hits + 1 - return cache[key] - } - - fun recreate(stopDeparturesByPlates: Set<Plate>) { - stopDeparturesByPlates.forEach { cache[key(it)] = it } - } - - init { - cache = cacheFromString(cachePreferences.all) - @Suppress("UNCHECKED_CAST") - cacheHits = cacheHitsPreferences.all as HashMap<String, Int> - } - - private fun cacheFromString(preferences: Map<String, *>): HashMap<String, Plate> { - val result = HashMap<String, Plate>() - for ((key, value) in preferences.entries) { - result[key] = Plate.fromString(value as String) - } - return result - } - - private fun key(plate: Plate) = "${plate.id.line}@${plate.id.stop}>${plate.id.headsign}" -} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt index 7fcb3e1ac4c36f0e0bcafc7fcc995bb468e920c7..8c39cb5c95f88327c5aa654d3d4b84e701fdcad6 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt @@ -31,7 +31,7 @@ if (intent != null) { notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val prefs = this.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE)!! - if (!NetworkStateReceiver.isNetworkAvailable(this)) { + if (!NetworkStateReceiver.isNetworkAvailable()) { sendResult(RESULT_NO_CONNECTIVITY) return } @@ -57,7 +57,6 @@ sendResult(RESULT_UP_TO_DATE) return } if (httpCon.responseCode != HttpsURLConnection.HTTP_OK) { - println(httpCon.responseMessage) sendResult(RESULT_NO_CONNECTIVITY) return } @@ -94,7 +93,7 @@ prefsEditor.putString("etag", newETag) prefsEditor.apply() val oldDb = File(getSecondaryExternalFilesDir(), "timetable.db") - gtfsDb.renameTo(oldDb) // todo<p:1> delete old before downloading (may require stopping VmClient), and mutex with VmClient + gtfsDb.renameTo(oldDb) // todo<p:1> delete old before downloading (may require stopping VmService), and mutex with VmService cancelNotification() diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt index bf703cca766ddb831b2069b1e778f3a979858694..af652ab3dad6fec82a7bb93d85733aac2cc10f56 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt @@ -1,256 +1,133 @@ package ml.adamsprogs.bimba.datasources -import android.app.Service -import android.content.Intent -import android.os.Handler -import android.os.HandlerThread -import android.os.IBinder -import android.os.Process.THREAD_PRIORITY_BACKGROUND -import com.google.gson.Gson +import com.google.gson.* +import kotlinx.coroutines.experimental.* import ml.adamsprogs.bimba.NetworkStateReceiver -import ml.adamsprogs.bimba.calendarFromIso -import ml.adamsprogs.bimba.models.* -import okhttp3.FormBody -import okhttp3.OkHttpClient -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId -import ml.adamsprogs.bimba.secondsAfterMidnight +import ml.adamsprogs.bimba.models.Plate +import ml.adamsprogs.bimba.models.StopSegment +import ml.adamsprogs.bimba.models.suggestions.* +import okhttp3.* import java.io.IOException import java.util.* import kotlin.collections.HashMap import kotlin.collections.HashSet -import kotlin.concurrent.thread -class VmClient : Service() { +class VmClient { companion object { - const val ACTION_READY = "ml.adamsprogs.bimba.action.vm.ready" - const val EXTRA_DEPARTURES = "ml.adamsprogs.bimba.extra.vm.departures" - const val EXTRA_PLATE_ID = "ml.adamsprogs.bimba.extra.vm.plate" - const val TICK_6_ZINA_TIM = 12500L - const val TICK_6_ZINA_TIM_WITH_MARGIN = TICK_6_ZINA_TIM * 3 / 4 - } + private var vmClient: VmClient? = null - private var handler: Handler? = null - private val tick6ZinaTim: Runnable = object : Runnable { - override fun run() { - handler!!.postDelayed(this, TICK_6_ZINA_TIM) - try { - for (plateId in requests.keys) - downloadVM() - } catch (e: IllegalArgumentException) { - } + fun getVmClient(): VmClient { + if (vmClient == null) + vmClient = VmClient() + return vmClient!! } } - private val requests = HashMap<AgencyAndId, Set<Request>>() - private val vms = HashMap<AgencyAndId, Set<Plate>>() - private val timetable = try { - Timetable.getTimetable(this) - } catch (e: NullPointerException) { - null - } - - override fun onCreate() { - val thread = HandlerThread("ServiceStartArguments", THREAD_PRIORITY_BACKGROUND) - thread.start() - handler = Handler(thread.looper) - handler!!.postDelayed(tick6ZinaTim, TICK_6_ZINA_TIM) - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (timetable == null) - return START_NOT_STICKY - val stopSegment = intent?.getParcelableExtra<StopSegment>("stop")!! - if (stopSegment.plates == null) - throw EmptyStopSegmentException() - val action = intent.action - val once = intent.getBooleanExtra("once", false) - if (action == "request") { - if (isAlreadyRequested(stopSegment)) { - incrementRequest(stopSegment) - sendResult(stopSegment) - } else { - if (!once) - addRequest(stopSegment) - thread { - downloadVM(stopSegment) - } - } - } else if (action == "remove") { - decrementRequest(stopSegment) - cleanRequests() + suspend fun getSheds(name: String): Map<String, Set<String>> { + val response = makeRequest("getBollardsByStopPoint", """{"name": "$name"}""") + if (!response.has("success")) + return emptyMap() + val rootObject = response["success"].asJsonObject["bollards"].asJsonArray + val result = HashMap<String, Set<String>>() + rootObject.forEach { + val code = it.asJsonObject["bollard"].asJsonObject["tag"].asString + result[code] = it.asJsonObject["directions"].asJsonArray.map { + """${it.asJsonObject["lineName"].asString} → ${it.asJsonObject["direction"].asString}""" + }.toSet() } - return START_STICKY + return result } - private fun cleanRequests() { - val newMap = HashMap<AgencyAndId, Set<Request>>() - requests.forEach { - newMap[it.key] = it.value.minus(it.value.filter { it.times == 0 }) - } - newMap.forEach { requests[it.key] = it.value } - } + /* + suspend fun getPlatesByStopPoint(code: String): Set<Plate.ID>? { + val getTimesResponse = makeRequest("getTimes", """{"symbol": "$code"}""") + val name = getTimesResponse["success"].asJsonObject["bollard"].asJsonObject["name"].asString - private fun addRequest(stopSegment: StopSegment) { - if (requests[stopSegment.stop] == null) { - requests[stopSegment.stop] = stopSegment.plates!! - .map { Request(it, 1) } - .toSet() - } else { - var req = requests[stopSegment.stop]!! - stopSegment.plates!!.forEach { - val plate = it - if (req.any { it.plate == plate }) { - req.filter { it.plate == plate }[0].times++ - } else { - req = req.plus(Request(it, 1)) - } - requests[stopSegment.stop] = req + val bollards = getBollardsByStopPoint(name) + return bollards.filter { + it.key == code + }.values.flatMap { + it.map { + val (line, headsign) = it.split(" → ") + Plate.ID(AgencyAndId(line), AgencyAndId(code), headsign) } + }.toSet() + }*/ + + suspend fun getStops(pattern: String): List<StopSuggestion> { + val response = withContext(DefaultDispatcher) { + makeRequest("getStopPoints", """{"pattern": "$pattern"}""") } - } - private fun sendResult(stop: StopSegment) { - vms[stop.stop]?.filter { - val plate = it - stop.plates!!.any { it == plate.id } - }?.forEach { sendResult(it.id, it.departures?.get(today())) } - } + if (!response.has("success")) + return emptyList() - private fun today(): AgencyAndId { - return timetable!!.getServiceForToday() - } + val points = response["success"].asJsonArray.map { it.asJsonObject } - private fun incrementRequest(stopSegment: StopSegment) { - stopSegment.plates!!.forEach { - val plateId = it - requests[it.stop]!!.filter { it.plate == plateId }.forEach { it.times++ } - } - } + val names = HashSet<String>() - private fun decrementRequest(stopSegment: StopSegment) { - stopSegment.plates!!.forEach { - val plateId = it - requests[it.stop]!!.filter { it.plate == plateId }.forEach { it.times-- } + points.forEach { + val name = it["name"].asString + names.add(name) } - } - private fun isAlreadyRequested(stopSegment: StopSegment): Boolean { - val platesIn = requests[stopSegment.stop]?.map { it.plate }?.toSet() - val platesOut = stopSegment.plates - if (platesIn == null || platesIn.isEmpty()) - return false - return (platesOut == platesIn || platesIn.containsAll(platesOut!!)) - } - - - override fun onBind(intent: Intent): IBinder? { - return null - } - - override fun onDestroy() { + return names.map { StopSuggestion(it, "", "") } } - @Synchronized - private fun downloadVM() { - vms.forEach { - downloadVM(StopSegment(it.key, it.value.map { it.id }.toSet())) - } - } + suspend fun makeRequest(method: String, data: String): JsonObject { + if (!NetworkStateReceiver.isNetworkAvailable()) + return JsonObject() - private fun downloadVM(stopSegment: StopSegment) { - if (!NetworkStateReceiver.isNetworkAvailable(this)) { - vms[stopSegment.stop] = HashSet(stopSegment.plates!!.map { Plate(it, null) }.toSet()) - stopSegment.plates!!.forEach { - sendResult(it, null) - } - return - } - - val stopSymbol = timetable!!.getStopCode(stopSegment.stop) val client = OkHttpClient() val url = "http://www.peka.poznan.pl/vm/method.vm?ts=${Calendar.getInstance().timeInMillis}" - val formBody = FormBody.Builder() - .add("method", "getTimes") - .add("p0", "{\"symbol\": \"$stopSymbol\"}") - .build() + val body = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded; charset=UTF-8"), + "method=$method&p0=$data") val request = okhttp3.Request.Builder() .url(url) - .post(formBody) + .post(body) .build() + val responseBody: String? try { - responseBody = client.newCall(request).execute().body()?.string() + responseBody = withContext(CommonPool) { + client.newCall(request).execute().body()?.string() + } } catch (e: IOException) { - stopSegment.plates!!.forEach { - sendResult(it, null) - } - return + return JsonObject() } - if (responseBody?.get(0) == '<') { - stopSegment.plates!!.forEach { - sendResult(it, null) - } - return + return try { + Gson().fromJson(responseBody, JsonObject::class.java) + } catch (e: JsonSyntaxException) { + JsonObject() } - - val javaRootMapObject = Gson().fromJson(responseBody, HashMap::class.java) - val times = (javaRootMapObject["success"] as Map<*, *>)["times"] as List<*> - stopSegment.plates!!.forEach { downloadVM(it, times) } - } - private fun downloadVM(plateId: Plate.ID, times: List<*>) { - val date = Calendar.getInstance() - val todayDay = "${date.get(Calendar.DATE)}".padStart(2, '0') - val todayMode = timetable!!.calendarToMode(AgencyAndId(timetable.getServiceForToday().id)) // fixme when no timetable use service == -1 for `today` - - val departures = HashSet<Departure>() - - times.forEach { - val thisLine = AgencyAndId((it as Map<*, *>)["line"] as String) - val thisHeadsign = it["direction"] as String - val thisPlateId = Plate.ID(thisLine, plateId.stop, thisHeadsign) - if (plateId == thisPlateId) { - val departureDay = (it["departure"] as String).split("T")[0].split("-")[2] - val departureTime = calendarFromIso(it["departure"] as String).secondsAfterMidnight() - val departure = Departure(plateId.line, todayMode, departureTime, false, - ArrayList(), it["direction"] as String, it["realTime"] as Boolean, - departureDay != todayDay, it["onStopPoint"] as Boolean) - departures.add(departure) - } + suspend fun getName(symbol: String): String? { + val timesResponse = withContext(DefaultDispatcher) { + makeRequest("getTimes", """{"symbol": "$symbol"}""") } + if (!timesResponse.has("success")) + return null - val departuresForPlate = HashMap<AgencyAndId, HashSet<Departure>>() - departuresForPlate[timetable.getServiceForToday()] = departures - val vm = vms[plateId.stop] ?: HashSet() - try { - (vm as HashSet).remove(vm.filter { it.id == plateId }[0]) - } catch (e: IndexOutOfBoundsException) { - } - (vm as HashSet).add(Plate(plateId, departuresForPlate)) - vms[plateId.stop] = vm - if (departures.isEmpty()) - sendResult(plateId, null) - else - sendResult(plateId, departures) + return timesResponse["success"].asJsonObject["bollard"].asJsonObject["name"].asString } - private fun sendResult(plateId: Plate.ID, departures: HashSet<Departure>?) { - val broadcastIntent = Intent() - broadcastIntent.action = ACTION_READY - broadcastIntent.addCategory(Intent.CATEGORY_DEFAULT) - if (departures != null) - broadcastIntent.putStringArrayListExtra(EXTRA_DEPARTURES, departures.map { it.toString() } as ArrayList) - broadcastIntent.putExtra(EXTRA_PLATE_ID, plateId) - sendBroadcast(broadcastIntent) - } + suspend fun getDirections(symbol: String): StopSegment? { + val name = getName(symbol) + val directionsResponse = makeRequest("getBollardsByStopPoint", """{"name": "$name"}""") - data class Request(val plate: Plate.ID, var times: Int) + if (!directionsResponse.has("success")) + return null - class EmptyStopSegmentException : Exception() + return StopSegment(symbol, + directionsResponse["success"].asJsonObject["bollards"].asJsonArray.filter { + it.asJsonObject["bollard"].asJsonObject["tag"].asString == symbol + }[0].asJsonObject["directions"].asJsonArray.map { + it.asJsonObject.let { direction -> + Plate.ID(direction["lineName"].asString, symbol, direction["direction"].asString) + } + }.toSet()) + } } - -//note application stops the service on exit - diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmService.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmService.kt new file mode 100644 index 0000000000000000000000000000000000000000..710aa49f6b3ad7a7f661864ea4231306aa403f20 --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmService.kt @@ -0,0 +1,189 @@ +package ml.adamsprogs.bimba.datasources + +import android.app.Service +import android.content.Intent +import android.os.* +import android.os.Process.THREAD_PRIORITY_BACKGROUND +import com.google.gson.JsonObject +import kotlinx.coroutines.experimental.android.UI +import kotlinx.coroutines.experimental.* +import ml.adamsprogs.bimba.NetworkStateReceiver +import ml.adamsprogs.bimba.calendarFromIso +import ml.adamsprogs.bimba.models.* +import ml.adamsprogs.bimba.secondsAfterMidnight +import java.util.* +import kotlin.collections.* + +class VmService : Service() { + companion object { + const val ACTION_READY = "ml.adamsprogs.bimba.action.vm.ready" + const val EXTRA_DEPARTURES = "ml.adamsprogs.bimba.extra.vm.departures" + const val EXTRA_PLATE_ID = "ml.adamsprogs.bimba.extra.vm.plate" + const val EXTRA_STOP_CODE = "ml.adamsprogs.bimba.extra.vm.stop" + const val TICK_6_ZINA_TIM = 12500L + const val TICK_6_ZINA_TIM_WITH_MARGIN = TICK_6_ZINA_TIM * 3 / 4 + } + + private var handler: Handler? = null + private val tick6ZinaTim: Runnable = object : Runnable { + override fun run() { + handler!!.postDelayed(this, TICK_6_ZINA_TIM) + try { + for (plateId in requests.keys) + launch(UI) { + withContext(DefaultDispatcher) { + downloadVM() + } + } + } catch (e: IllegalArgumentException) { + } + } + } + private val requests = HashMap<String, Int>() + private val vms = HashMap<String, Set<Plate>>() + + override fun onCreate() { + val thread = HandlerThread("ServiceStartArguments", THREAD_PRIORITY_BACKGROUND) + thread.start() + handler = Handler(thread.looper) + handler!!.postDelayed(tick6ZinaTim, TICK_6_ZINA_TIM) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent == null) + return START_STICKY + val stopCode = intent.getStringExtra("stop")!! + val action = intent.action + val once = intent.getBooleanExtra("once", false) + if (action == "request") { + if (isAlreadyRequested(stopCode)) { + incrementRequest(stopCode) + sendResult(stopCode) + } else { + if (!once) + addRequest(stopCode) + launch(UI) { + withContext(DefaultDispatcher) { + downloadVM(stopCode) + } + } + } + } else if (action == "remove") { + decrementRequest(stopCode) + cleanRequests() + } + return START_STICKY + } + + private fun cleanRequests() { + requests.forEach { + if (it.value <= 0) + requests.remove(it.key) + } + } + + private fun addRequest(stopCode: String) { + if (requests[stopCode] == null) + requests[stopCode] = 0 + requests[stopCode] = requests[stopCode]!! + 1 + } + + private fun incrementRequest(stopCode: String) { + requests[stopCode] = requests[stopCode]!! + 1 + } + + private fun decrementRequest(stopCode: String) { + requests[stopCode] = requests[stopCode]!! - 1 + } + + private fun isAlreadyRequested(stopCode: String): Boolean { + return stopCode in requests + } + + + override fun onBind(intent: Intent): IBinder? { + return null + } + + override fun onDestroy() { + } + + private suspend fun downloadVM() { + vms.forEach { + downloadVM(it.key) + } + } + + private suspend fun downloadVM(stopCode: String) { + if (!NetworkStateReceiver.isNetworkAvailable()) { + vms[stopCode] = emptySet() + sendResult(stopCode, null, null) + return + } + + val javaRootMapObject = VmClient.getVmClient().makeRequest("getTimes", """{"symbol": "$stopCode"}""") + + if (!javaRootMapObject.has("success")) { + sendResult(stopCode, null, null) + return + } + + val times = (javaRootMapObject["success"].asJsonObject)["times"].asJsonArray.map { it.asJsonObject } + parseTimes(stopCode, times) + } + + private fun parseTimes(stopCode: String, times: List<JsonObject>) { + val date = Calendar.getInstance() + val todayDay = "${date.get(Calendar.DATE)}".padStart(2, '0') + + val departures = HashMap<Plate.ID, HashSet<Departure>>() + + times.forEach { + val thisLine = it["line"].asString + val thisHeadsign = it["direction"].asString + val thisPlateId = Plate.ID(thisLine, stopCode, thisHeadsign) + if (departures[thisPlateId] == null) + departures[thisPlateId] = HashSet() + val departureDay = (it["departure"].asString).split("T")[0].split("-")[2] + val departureTime = calendarFromIso(it["departure"].asString).secondsAfterMidnight() + val departure = Departure(thisLine, listOf(-1), departureTime, false, + ArrayList(), it["direction"].asString, it["realTime"].asBoolean, + departureDay != todayDay, it["onStopPoint"].asBoolean) + departures[thisPlateId]!!.add(departure) + } + + departures.forEach { + val departuresForPlate = HashMap<Int, HashSet<Departure>>() + departuresForPlate[-1] = it.value + val vm = HashSet<Plate>() + vm.add(Plate(it.key, departuresForPlate)) + vms[stopCode] = vm + if (departures.isEmpty()) + sendResult(stopCode, it.key, null) + else + sendResult(stopCode, it.key, it.value) + } + + } + + private fun sendResult(stopCode: String) { + vms[stopCode]?.forEach { + sendResult(it.id.stop, it.id, it.departures?.get(-1)) + } + + } + + private fun sendResult(stopCode: String, plateId: Plate.ID?, departures: HashSet<Departure>?) { + val broadcastIntent = Intent() + broadcastIntent.action = ACTION_READY + broadcastIntent.addCategory(Intent.CATEGORY_DEFAULT) + if (departures != null) + broadcastIntent.putStringArrayListExtra(EXTRA_DEPARTURES, departures.map { it.toString() } as ArrayList) + broadcastIntent.putExtra(EXTRA_PLATE_ID, plateId) + broadcastIntent.putExtra(EXTRA_STOP_CODE, stopCode) + sendBroadcast(broadcastIntent) + } +} + +//note application stops the service on exit + diff --git a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt index cc04631c1708de25c1758299bc19ba0978599b2c..a349dc85e76661e7745d7245c11cf93a3d450e75 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import android.graphics.drawable.Drawable import android.os.Build +import android.text.format.DateFormat import ml.adamsprogs.bimba.activities.StopActivity import java.io.* import java.text.SimpleDateFormat @@ -80,7 +81,9 @@ else -> StopActivity.MODE_WORKDAYS } } -internal fun CharSequence.safeSplit(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List<String> { +internal fun CharSequence.safeSplit(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List<String>? { + if (this == "null") + return null if (this == "") return ArrayList() return this.split(*delimiters, ignoreCase = ignoreCase, limit = limit) @@ -104,3 +107,23 @@ bytes = read(buffer) } return bytesCopied } + +internal fun Calendar.toNiceString(context: Context, withTime: Boolean = false): String { + val dateFormat = DateFormat.getMediumDateFormat(context) + val timeFormat = DateFormat.getTimeFormat(context) + val now = Calendar.getInstance() + val date = if (get(Calendar.YEAR) == now.get(Calendar.YEAR)) { + when { + get(Calendar.DAY_OF_YEAR) == now.get(Calendar.DAY_OF_YEAR) -> timeFormat.format(time) + now.apply { add(Calendar.DATE, -1) }.get(Calendar.DAY_OF_YEAR) == get(Calendar.DAY_OF_YEAR) -> "Yesterday" + else -> DateFormat.format("d MMM" as CharSequence, this.time) as String + } + } else + dateFormat.format(this.time) + + return if (withTime) { + val time = timeFormat.format(this.time) + "$date, $time" + } else + date +} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt index 9989a5a13126f8a19094985c47c289382071c70b..9661f529ba9bd5d83a822b47ec43e022c1daa537 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt @@ -1,6 +1,5 @@ package ml.adamsprogs.bimba.models -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId import ml.adamsprogs.bimba.safeSplit import ml.adamsprogs.bimba.secondsAfterMidnight import java.io.Serializable @@ -8,7 +7,7 @@ import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap -data class Departure(val line: AgencyAndId, val mode: List<Int>, val time: Int, val lowFloor: Boolean, //time in seconds since midnight +data class Departure(val line: String, val mode: List<Int>, val time: Int, val lowFloor: Boolean, //time in seconds since midnight val modification: List<String>, val headsign: String, val vm: Boolean = false, var tomorrow: Boolean = false, val onStop: Boolean = false) { @@ -28,7 +27,7 @@ companion object { private fun filterDepartures(departures: List<Departure>, relativeTo: Int = Calendar.getInstance().secondsAfterMidnight()): Array<Serializable> { val filtered = ArrayList<Departure>() - val lines = HashMap<AgencyAndId, Int>() + val lines = HashMap<String, Int>() val sortedDepartures = departures.sortedBy { it.timeTill(relativeTo) } for (departure in sortedDepartures) { val timeTill = departure.timeTill(relativeTo) @@ -42,47 +41,20 @@ } return arrayOf(filtered, lines.all { it.value >= 3 }) } - fun createDepartures(stopId: AgencyAndId): Map<AgencyAndId, List<Departure>> { + /*fun createDepartures(stopCode: String): Map<String, List<Departure>> { val timetable = Timetable.getTimetable() - val departures = timetable.getStopDepartures(stopId) + val departures = timetable.getStopDepartures(stopCode) return rollDepartures(departures) - } - - fun rollDepartures(departures: Map<AgencyAndId, List<Departure>>): Map<AgencyAndId, List<Departure>> { //todo<p:2> it'd be nice to roll from tomorrow's real mode (Fri->Sat, Sat->Sun, Sun->Mon) - val rolledDepartures = HashMap<AgencyAndId, List<Departure>>() - departures.keys.forEach { - val (filtered, isFull) = filterDepartures(departures[it]!!) - if (isFull as Boolean) { - @Suppress("UNCHECKED_CAST") - rolledDepartures[it] = filtered as List<Departure> - } else { - val (filteredTomorrow, _) = filterDepartures(departures[it]!!, 0) - val departuresTomorrow = ArrayList<Departure>() - @Suppress("UNCHECKED_CAST") - (filteredTomorrow as List<Departure>).forEach { - val departure = it.copy() - departure.tomorrow = true - departuresTomorrow.add(departure) - } - val (result, _) = - @Suppress("UNCHECKED_CAST") - filterDepartures((filtered as List<Departure>) + departuresTomorrow) - val now = Calendar.getInstance().secondsAfterMidnight() - @Suppress("UNCHECKED_CAST") - rolledDepartures[it] = (result as List<Departure>).sortedBy { it.timeTill(now) } - } - } - return rolledDepartures - } + }*/ fun fromString(string: String): Departure { val array = string.split("|") if (array.size != 9) throw IllegalArgumentException() - val modification = array[4].safeSplit(";") - return Departure(AgencyAndId.convertFromString(array[0]), - array[1].safeSplit(";").map { Integer.parseInt(it) }, + val modification = array[4].safeSplit(";")!! + return Departure(array[0], + array[1].safeSplit(";")!!.map { Integer.parseInt(it) }, Integer.parseInt(array[2]), array[3] == "true", modification, array[5], array[6] == "true", array[7] == "true", array[8] == "true") @@ -96,5 +68,5 @@ time += 24 * 60 * 60 return (time - relativeTo) / 60 } - val lineText: String = line.id + val lineText: String = line } \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt index 469a7cdc28422e9fbc937c2e77c7fc4a59f2242b..8e2a99c5f289cabf1caacd451ff3bd91cb26d5d1 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt @@ -3,42 +3,28 @@ import android.content.* import android.os.* import ml.adamsprogs.bimba.* -import ml.adamsprogs.bimba.datasources.VmClient -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId import java.io.File import java.math.BigInteger import java.security.SecureRandom -import java.util.Calendar import kotlin.collections.* -class Favourite : Parcelable, MessageReceiver.OnVmListener { - private var isRegisteredOnVmListener: Boolean = false +class Favourite : Parcelable, ProviderProxy.OnDeparturesReadyListener { private val cacheDir: File + private lateinit var listener: ProviderProxy.OnDeparturesReadyListener var name: String private set var segments: HashSet<StopSegment> private set - private var vmDepartures = HashMap<Plate.ID, List<Departure>>() - var fullDepartures: Map<AgencyAndId, List<Departure>> = HashMap() - private set - val timetable = Timetable.getTimetable() + private var fullDepartures: Map<String, List<Departure>> = HashMap() + private var cache: List<Departure> = ArrayList() + private var listenerId = "" val size get() = segments.sumBy { it.size } - val isBackedByVm - get() = vmDepartures.isNotEmpty() - private val onVmPreparedListeners = HashSet<OnVmPreparedListener>() - - fun addOnVmPreparedListener(listener: OnVmPreparedListener) { - onVmPreparedListeners.add(listener) - } - - fun removeOnVmPreparedListener(listener: OnVmPreparedListener) { - onVmPreparedListeners.remove(listener) - } + private val providerProxy: ProviderProxy constructor(parcel: Parcel) { this.name = parcel.readString() @@ -54,26 +40,29 @@ val mapDir = File(parcel.readString()) val mapString = mapDir.readText() - val map = HashMap<AgencyAndId, List<Departure>>() - mapString.safeSplit("%").forEach { + val map = HashMap<String, List<Departure>>() + mapString.safeSplit("%")!!.forEach { it -> val (k, v) = it.split("#") - map[AgencyAndId(k)] = v.split("&").map { Departure.fromString(it) } + map[k] = v.split("&").map { Departure.fromString(it) } } this.fullDepartures = map mapDir.delete() + providerProxy = ProviderProxy() } - constructor(name: String, segments: HashSet<StopSegment>, cache: Map<AgencyAndId, List<Departure>>, context: Context) { + constructor(name: String, segments: HashSet<StopSegment>, cache: Map<String, List<Departure>>, context: Context) { this.fullDepartures = cache this.name = name this.segments = segments this.cacheDir = context.cacheDir + providerProxy = ProviderProxy(context) } constructor(name: String, timetables: HashSet<StopSegment>, context: Context) { this.name = name this.segments = timetables this.cacheDir = context.cacheDir + providerProxy = ProviderProxy(context) } @@ -94,7 +83,7 @@ dest?.writeString(mapFile.absolutePath) var isFirst = true var map = "" - fullDepartures.forEach { + fullDepartures.forEach { it -> if (isFirst) isFirst = false else @@ -105,49 +94,13 @@ } mapFile.writeText(map) } - private fun filterVmDepartures() { - val now = Calendar.getInstance().secondsAfterMidnight() - this.vmDepartures.forEach { - val newVms = it.value - .filter { it.timeTill(now) >= 0 }.sortedBy { it.timeTill(now) } - this.vmDepartures[it.key] = newVms - } - } - - fun delete(plateId: Plate.ID) { + fun delete(plateId: Plate.ID): Boolean { segments.forEach { - it.remove(plateId) + if (!it.remove(plateId)) + return false } removeFromCache(plateId) - } - - fun registerOnVm(receiver: MessageReceiver, context: Context) { - if (!isRegisteredOnVmListener) { - receiver.addOnVmListener(this) - isRegisteredOnVmListener = true - - - segments.forEach { - val intent = Intent(context, VmClient::class.java) - intent.putExtra("stop", it) - intent.action = "request" - context.startService(intent) - } - } - } - - fun deregisterOnVm(receiver: MessageReceiver, context: Context) { - if (isRegisteredOnVmListener) { - receiver.removeOnVmListener(this) - isRegisteredOnVmListener = false - - segments.forEach { - val intent = Intent(context, VmClient::class.java) - intent.putExtra("stop", it) - intent.action = "remove" - context.startService(intent) - } - } + return true } fun rename(newName: String) { @@ -164,80 +117,40 @@ return arrayOfNulls(size) } } - fun nextDeparture(): Departure? { - val now = Calendar.getInstance().secondsAfterMidnight() - filterVmDepartures() - if (segments.isEmpty() && vmDepartures.isEmpty()) - return null - - if (vmDepartures.isNotEmpty()) { - return vmDepartures.flatMap { it.value } - .minBy { - it.timeTill(now) - } - } + fun nextDeparture() = + if (cache.isEmpty()) + null + else + cache.sortedBy { it.time }[0] - val full = fullTimetable() - val twoDayDepartures = try { - Departure.rollDepartures(full)[timetable.getServiceForToday()] - } catch (e: IllegalArgumentException) { - listOf<Departure>() - } - - if (twoDayDepartures?.isEmpty() != false) - return null - - return twoDayDepartures[0] + fun fullTimetable(): Map<String, List<Departure>> { + if (fullDepartures.isEmpty()) + fullDepartures = providerProxy.getFullTimetable(segments) + return fullDepartures } - fun allDepartures(): Map<AgencyAndId, List<Departure>> { - if (vmDepartures.isNotEmpty()) { - val now = Calendar.getInstance().secondsAfterMidnight() - val departures = HashMap<AgencyAndId, List<Departure>>() - val today = timetable.getServiceForToday() - departures[today] = vmDepartures.flatMap { it.value }.sortedBy { it.timeTill(now) } - return departures + private fun removeFromCache(plate: Plate.ID) { + val map = HashMap<String, List<Departure>>() + fullDepartures + fullDepartures.forEach { it -> + map[it.key] = it.value.filter { plate.line != it.line || plate.headsign != it.headsign } } - - val departures = fullTimetable() - return Departure.rollDepartures(departures) + fullDepartures = map } - fun fullTimetable() = - if (fullDepartures.isNotEmpty()) - fullDepartures - else { - fullDepartures = timetable.getStopDeparturesBySegments(segments) - fullDepartures - - } - - - override fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID) { - val now = Calendar.getInstance().secondsAfterMidnight() - if (segments.any { it.contains(plateId) }) { - if (vmDepartures == null) - this.vmDepartures.remove(plateId) - else - this.vmDepartures[plateId] = vmDepartures.sortedBy { it.timeTill(now) } - } - filterVmDepartures() - onVmPreparedListeners.forEach { - it.onVmPrepared() - } + fun subscribeForDepartures(listener: ProviderProxy.OnDeparturesReadyListener, context: Context): String { + this.listener = listener + listenerId = providerProxy.subscribeForDepartures(segments, this, context) + return listenerId } - private fun removeFromCache(plate: Plate.ID) { - val map = HashMap<AgencyAndId, List<Departure>>() - fullDepartures - fullDepartures.forEach { - map[it.key] = it.value.filter { plate.line != it.line || plate.headsign != it.headsign } - } - fullDepartures = map + override fun onDeparturesReady(departures: List<Departure>, plateId: Plate.ID?) { + cache = departures + listener.onDeparturesReady(departures, plateId) } - interface OnVmPreparedListener { - fun onVmPrepared() + fun unsubscribeFromDepartures(context: Context) { + providerProxy.unsubscribeFromDepartures(listenerId, context) } } diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt index 43ff657d166e562c108df2eeacc2aa2031be0cd1..d4f7d8cc0dd3dc9e147c63f4182cbc1e009c64e7 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt @@ -1,9 +1,8 @@ package ml.adamsprogs.bimba.models -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId import java.io.Serializable -data class Plate(val id: ID, val departures: HashMap<AgencyAndId, HashSet<Departure>>?) { +data class Plate(val id: ID, val departures: HashMap<Int, HashSet<Departure>>?) { override fun toString(): String { var result = "${id.line}=${id.stop}=${id.headsign}={" if (departures != null) { @@ -19,29 +18,26 @@ return result } companion object { - fun fromString(string: String): Plate { + /*fun fromString(string: String): Plate { val (lineStr, stopStr, headsign, departuresString) = string.split("=") - val line = AgencyAndId.convertFromString(lineStr) - val stop = AgencyAndId.convertFromString(stopStr) - val departures = HashMap<AgencyAndId, HashSet<Departure>>() + val departures = HashMap<Int, HashSet<Departure>>() departuresString.replace("{", "").replace("}", "").split(";") .filter { it != "" } .forEach { try { val (serviceStr, depStr) = it.split(":") val dep = Departure.fromString(depStr) - val service = AgencyAndId.convertFromString(serviceStr) - if (departures[service] == null) - departures[service] = HashSet() - departures[service]!!.add(dep) + if (departures[serviceStr] == null) + departures[serviceStr] = HashSet() + departures[serviceStr]!!.add(dep) } catch (e: IllegalArgumentException) { } } - return Plate(ID(line, stop, headsign), departures) + return Plate(ID(lineStr, stopStr, headsign), departures) } - fun join(set: Set<Plate>): HashMap<AgencyAndId, ArrayList<Departure>> { - val departures = HashMap<AgencyAndId, ArrayList<Departure>>() + fun join(set: Set<Plate>): HashMap<String, ArrayList<Departure>> { + val departures = HashMap<String, ArrayList<Departure>>() for (plate in set) { for ((mode, d) in plate.departures!!) { if (departures[mode] == null) @@ -53,15 +49,17 @@ for ((mode, _) in departures) { departures[mode]?.sortBy { it.time } } return departures - } + }*/ } - data class ID(val line: AgencyAndId, val stop: AgencyAndId, val headsign: String) : Serializable { + data class ID(val line: String, val stop: String, val headsign: String) : Serializable { companion object { + val dummy = Plate.ID("", "", "") + fun fromString(string: String): ID { val (line, stop, headsign) = string.split("|") - return ID(AgencyAndId.convertFromString(line), - AgencyAndId.convertFromString(stop), headsign) + return ID(line, + stop, headsign) } } diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt b/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt index 5197e6e17237a3b8ab3e28880b875321ddb6fda3..50f2fb4860b39f77025541cb341a46a18ab93e2c 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt @@ -2,13 +2,12 @@ package ml.adamsprogs.bimba.models import android.os.Parcel import android.os.Parcelable -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId import ml.adamsprogs.bimba.safeSplit -data class StopSegment(val stop: AgencyAndId, var plates: Set<Plate.ID>?) : Parcelable { +data class StopSegment(val stop: String, var plates: Set<Plate.ID>?) : Parcelable { constructor(parcel: Parcel) : this( - parcel.readSerializable() as AgencyAndId, - parcel.readString().safeSplit(";").map { Plate.ID.fromString(it) }.toSet() + parcel.readSerializable() as String, + parcel.readString().safeSplit(";")?.map { Plate.ID.fromString(it) }?.toSet() ) companion object CREATOR : Parcelable.Creator<StopSegment> { @@ -21,16 +20,12 @@ return arrayOfNulls(size) } } - fun fillPlates() { - plates = Timetable.getTimetable().getPlatesForStop(stop) - } - override fun writeToParcel(dest: Parcel?, flags: Int) { dest?.writeSerializable(stop) if (plates != null) dest?.writeString(plates!!.joinToString(";") { it.toString() }) else - dest?.writeString("") + dest?.writeString("null") } override fun describeContents(): Int { @@ -56,17 +51,33 @@ return false } override fun hashCode(): Int { - return super.hashCode() + var hashCode = stop.hashCode() + plates?.forEach { hashCode = 31 * hashCode + it.hashCode() } + return hashCode + } + + operator fun contains(plateId: Plate.ID): Boolean { + if (plates == null) + return plateId.stop == stop + return plates!!.contains(plateId) } - fun contains(plateId: Plate.ID): Boolean { + fun remove(plateId: Plate.ID): Boolean { if (plates == null) return false - return plates!!.contains(plateId) + return (plates as HashSet).remove(plateId) } - fun remove(plateId: Plate.ID) { - (plates as HashSet).remove(plateId) + override fun toString(): String { + var s = "$stop: " + if (plates == null) + s += "NULL" + else { + s += "{" + s += plates!!.joinToString { it.toString() } + s += "}" + } + return s } val size: Int diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt index ceed419ddda0ed2bca34443399301c56f5818792..3903ffbcec24fc67e2067efcf815c75e40d1545d 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt @@ -4,6 +4,9 @@ import android.annotation.SuppressLint import android.content.Context import android.database.* import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteException +import android.util.SparseArray +import android.util.SparseBooleanArray import ml.adamsprogs.bimba.* import ml.adamsprogs.bimba.models.gtfs.* import ml.adamsprogs.bimba.models.suggestions.* @@ -37,53 +40,54 @@ private fun constructTimetable(context: Context) { val timetable = Timetable() val filesDir = context.getSecondaryExternalFilesDir() val dbFile = File(filesDir, "timetable.db") - timetable.db = SQLiteDatabase.openDatabase(dbFile.path, null, SQLiteDatabase.OPEN_READONLY) // fixme can be null + timetable.db = try { + SQLiteDatabase.openDatabase(dbFile.path, null, SQLiteDatabase.OPEN_READONLY) + } catch (e: SQLiteException) { + null + } this.timetable = timetable } } - private lateinit var db: SQLiteDatabase + private var db: SQLiteDatabase? = null private var _stops: List<StopSuggestion>? = null fun refresh() { } - fun getStopSuggestions(context: Context, force: Boolean = false): List<StopSuggestion> { + fun getStopSuggestions(/*context: Context, */force: Boolean = false): List<StopSuggestion> { if (_stops != null && !force) return _stops!! - val ids = HashMap<String, HashSet<AgencyAndId>>() val zones = HashMap<String, String>() - val cursor = db.rawQuery("select stop_name, stop_id, zone_id from stops", null) + val cursor = db!!.rawQuery("select stop_name, zone_id from stops", null) while (cursor.moveToNext()) { val name = cursor.getString(0) - val id = cursor.getInt(1) - val zone = cursor.getString(2) - if (name !in ids) - ids[name] = HashSet() - ids[name]!!.add(AgencyAndId(id.toString())) + val zone = cursor.getString(1) zones[name] = zone } cursor.close() - _stops = ids.map { + _stops = zones.map { + /*todo val colour = when (zones[it.key]) { "A" -> "#${getColour(R.color.zoneA, context).toString(16)}" "B" -> "#${getColour(R.color.zoneB, context).toString(16)}" "C" -> "#${getColour(R.color.zoneC, context).toString(16)}" else -> "#000000" } - StopSuggestion(it.key, it.value, zones[it.key]!!, colour) + */ + StopSuggestion(it.key, it.value, "#000000") }.sorted() return _stops!! } fun getLineSuggestions(): List<LineSuggestion> { val routes = ArrayList<LineSuggestion>() - val cursor = db.rawQuery("select * from routes", null) + val cursor = db!!.rawQuery("select * from routes", null) while (cursor.moveToNext()) { val routeId = cursor.getString(0) @@ -95,31 +99,35 @@ return routes.sortedBy { it.name } } - fun getHeadlinesForStop(stops: Set<AgencyAndId>): Map<AgencyAndId, Pair<String, Set<String>>> { - val headsigns = HashMap<AgencyAndId, Pair<String, HashSet<String>>>() + fun getHeadlinesForStop(stop: String): Map<String, Set<String>> { + val headsigns = HashMap<String, HashSet<String>>() - val stopsIndex = HashMap<Int, String>() - val where = stops.joinToString(" or ", "where ") { "stop_id = ?" } - var cursor = db.rawQuery("select stop_id, stop_code from stops $where", stops.map { it.toString() }.toTypedArray()) - + var cursor = db!!.rawQuery("select stop_id, stop_code from stops where stop_name = ?", + arrayOf(stop)) + val stopIds = ArrayList<String>() + val stopCodes = SparseArray<String>() while (cursor.moveToNext()) { - stopsIndex[cursor.getInt(0)] = cursor.getString(1) + cursor.getInt(0).let { + stopIds.add(it.toString()) + stopCodes.put(it, cursor.getString(1)) + } } cursor.close() - cursor = db.rawQuery("select stop_id, route_id, trip_headsign " + + val where = stopIds.joinToString(" or ", "where ") { "stop_id = ?" } + + cursor = db!!.rawQuery("select stop_id, route_id, trip_headsign " + "from stop_times natural join trips " + - where, stops.map { it.toString() }.toTypedArray()) + where, stopIds.toTypedArray()) while (cursor.moveToNext()) { - val stop = cursor.getInt(0) - val stopId = AgencyAndId(stop.toString()) + val stopCode = stopCodes[cursor.getInt(0)] val route = cursor.getString(1) val headsign = cursor.getString(2) - if (stopId !in headsigns) - headsigns[stopId] = Pair(stopsIndex[stop]!!, HashSet()) - headsigns[stopId]!!.second.add("$route → $headsign") + if (stopCode !in headsigns) + headsigns[stopCode] = HashSet() + headsigns[stopCode]!!.add("$route → $headsign") } cursor.close() @@ -127,19 +135,42 @@ return headsigns /* - 1435 -> (AWF03, {232 → Os. Rusa}) - 1436 -> (AWF04, {232 → Rondo Kaponiera}) - 1437 -> (AWF02, {76 → Pl. Bernardyński, 74 → Os. Sobieskiego, 603 → Pl. Bernardyński}) - 1634 -> (AWF01, {76 → Os. Dębina, 603 → Łęczyca/Dworcowa}) - 171 -> (AWF42, {29 → Pl. Wiosny Ludów}) - 172 -> (AWF41, {10 → Połabska, 29 → Dębiec, 15 → Budziszyńska, 10 → Dębiec, 15 → Os. Sobieskiego, 12 → Os. Sobieskiego, 6 → Junikowo, 18 → Ogrody, 2 → Ogrody}) - 4586 -> (AWF73, {10 → Franowo, 29 → Franowo, 6 → Miłostowo, 5 → Stomil, 18 → Franowo, 15 → Franowo, 12 → Starołęka, 74 → Os. Orła Białego}) + AWF03 -> {232 → Os. Rusa} + AWF04 -> {232 → Rondo Kaponiera} + AWF02 -> {76 → Pl. Bernardyński, 74 → Os. Sobieskiego, 603 → Pl. Bernardyński} + AWF01 ->{76 → Os. Dębina, 603 → Łęczyca/Dworcowa} + AWF42 -> {29 → Pl. Wiosny Ludów} + AWF41 -> {10 → Połabska, 29 → Dębiec, 15 → Budziszyńska, 10 → Dębiec, 15 → Os. Sobieskiego, 12 → Os. Sobieskiego, 6 → Junikowo, 18 → Ogrody, 2 → Ogrody} + AWF73 -> {10 → Franowo, 29 → Franowo, 6 → Miłostowo, 5 → Stomil, 18 → Franowo, 15 → Franowo, 12 → Starołęka, 74 → Os. Orła Białego} */ } - fun getStopName(stopId: AgencyAndId): String { - val cursor = db.rawQuery("select stop_name from stops where stop_id = ?", - arrayOf(stopId.id)) + fun getHeadlinesForStopCode(stop: String): StopSegment { + var cursor = db!!.rawQuery("select stop_id from stops where stop_code = ?", + arrayOf(stop)) + cursor.moveToFirst() + val stopId = cursor.getInt(0) + cursor.close() + + + cursor = db!!.rawQuery("select route_id, trip_headsign " + + "from stop_times natural join trips where stop_id = ? ", + arrayOf(stopId.toString())) + + val plates = HashSet<Plate.ID>() + + while (cursor.moveToNext()) { + val route = cursor.getString(0) + val headsign = cursor.getString(1) + plates.add(Plate.ID(route, stop, headsign)) + } + cursor.close() + return StopSegment(stop, plates) + } + + fun getStopName(stopCode: String): String { + val cursor = db!!.rawQuery("select stop_name from stops where stop_code = ?", + arrayOf(stopCode)) cursor.moveToNext() val name = cursor.getString(0) cursor.close() @@ -147,9 +178,19 @@ return name } - fun getStopCode(stopId: AgencyAndId): String { - val cursor = db.rawQuery("select stop_code from stops where stop_id = ?", - arrayOf(stopId.id)) + fun getStopId(stopCode: String): String { + val cursor = db!!.rawQuery("select stop_id from stops where stop_code = ?", + arrayOf(stopCode)) + cursor.moveToNext() + val id = cursor.getString(0) + cursor.close() + + return id + } + + fun getStopCode(stopId: String): String { + val cursor = db!!.rawQuery("select stop_code from stops where stop_id = ?", + arrayOf(stopId)) cursor.moveToNext() val code = cursor.getString(0) cursor.close() @@ -157,16 +198,17 @@ return code } - fun getStopDepartures(stopId: AgencyAndId): Map<AgencyAndId, List<Departure>> { - val map = HashMap<AgencyAndId, ArrayList<Departure>>() - val cursor = db.rawQuery("select route_id, service_id, departure_time, " + + fun getStopDepartures(stopCode: String): Map<String, List<Departure>> { + val stopID = getStopId(stopCode) + val map = HashMap<String, ArrayList<Departure>>() + val cursor = db!!.rawQuery("select route_id, service_id, departure_time, " + "wheelchair_accessible, stop_sequence, trip_id, trip_headsign, route_desc " + "from stop_times natural join trips natural join routes where stop_id = ?", - arrayOf(stopId.id)) + arrayOf(stopID)) while (cursor.moveToNext()) { - val line = AgencyAndId(cursor.getString(0)) - val service = AgencyAndId(cursor.getInt(1).toString()) + val line = cursor.getString(0) + val service = cursor.getInt(1).toString() val mode = calendarToMode(service) val time = parseTime(cursor.getString(2)) val lowFloor = cursor.getInt(3) == 1 @@ -190,14 +232,21 @@ return map } - fun getStopDeparturesBySegments(segments: HashSet<StopSegment>): Map<AgencyAndId, List<Departure>> { + fun getStopDeparturesBySegments(segments: Set<StopSegment>): Map<String, List<Departure>> { + val stopCodes = HashMap<String, Int>() + var cursor = db!!.rawQuery("select stop_id, stop_code from stops", emptyArray()) + while (cursor.moveToNext()) { + stopCodes[cursor.getString(1)] = cursor.getInt(0) + } + cursor.close() + val wheres = segments.flatMap { - it.plates?.map { - "(stop_id = ${it.stop} and route_id = '${it.line}' and trip_headsign = '${it.headsign}')" - } ?: listOf() + it.plates?.map { plate -> + "(stop_id = ${stopCodes[plate.stop]} and route_id = '${plate.line}' and trip_headsign = '${plate.headsign}')" + } ?: listOf("stop_id = ${stopCodes[it.stop]}") }.joinToString(" or ") - val cursor = db.rawQuery("select route_id, service_id, departure_time, " + + cursor = db!!.rawQuery("select route_id, service_id, departure_time, " + "wheelchair_accessible, stop_sequence, trip_id, trip_headsign, route_desc " + "from stop_times natural join trips natural join routes where $wheres", null) @@ -206,12 +255,12 @@ cursor.close() return map } - private fun parseDeparturesCursor(cursor: Cursor): Map<AgencyAndId, List<Departure>> { - val map = HashMap<AgencyAndId, ArrayList<Departure>>() + private fun parseDeparturesCursor(cursor: Cursor): Map<String, List<Departure>> { + val map = HashMap<String, ArrayList<Departure>>() while (cursor.moveToNext()) { - val line = AgencyAndId(cursor.getString(0)) - val service = AgencyAndId(cursor.getInt(1).toString()) + val line = cursor.getString(0) + val service = cursor.getInt(1).toString() val mode = calendarToMode(service) val time = parseTime(cursor.getString(2)) val lowFloor = cursor.getInt(3) == 1 @@ -243,10 +292,10 @@ cal.set(JCalendar.SECOND, s.toInt()) return cal.secondsAfterMidnight() } - fun calendarToMode(serviceId: AgencyAndId): List<Int> { + private fun calendarToMode(serviceId: String): List<Int> { val days = ArrayList<Int>() - val cursor = db.rawQuery("select * from calendar where service_id = ?", - arrayOf(serviceId.id)) + val cursor = db!!.rawQuery("select * from calendar where service_id = ?", + arrayOf(serviceId)) cursor.moveToNext() (1 until 7).forEach { @@ -262,9 +311,9 @@ val explanations = ArrayList () tripId.modification.forEach { if (it.stopRange != null) { if (stopSequence in it.stopRange) - explanations.add(routeModifications[it.id.id]!!) + explanations.add(routeModifications[it.id]!!) } else { - explanations.add(routeModifications[it.id.id]!!) + explanations.add(routeModifications[it.id]!!) } } @@ -295,23 +344,25 @@ if (modification != "") { modification.split(",").forEach { try { val (id, start, end) = it.split(":") - modifications.add(Trip.ID.Modification(AgencyAndId(id), IntRange(start.toInt(), end.toInt()))) + modifications.add(Trip.ID.Modification(id, IntRange(start.toInt(), end.toInt()))) } catch (e: Exception) { - modifications.add(Trip.ID.Modification(AgencyAndId(it), null)) + modifications.add(Trip.ID.Modification(it, null)) } } } - return Trip.ID(rawId, AgencyAndId(rawId.split("^")[0]), modifications, isMain) + return Trip.ID(rawId, rawId.split("^")[0], modifications, isMain) } else - return Trip.ID(rawId, AgencyAndId(rawId), HashSet(), false) + return Trip.ID(rawId, rawId, HashSet(), false) } @SuppressLint("Recycle") fun isEmpty(): Boolean { + if (db == null) + return true var result: Boolean var cursor: Cursor? = null try { - cursor = db.rawQuery("select * from feed_info", null) + cursor = db!!.rawQuery("select * from feed_info", null) result = !cursor.moveToNext() } catch (e: Exception) { result = true @@ -322,7 +373,7 @@ return result } fun getValidSince(): String { - val cursor = db.rawQuery("select feed_start_date from feed_info", null) + val cursor = db!!.rawQuery("select feed_start_date from feed_info", null) cursor.moveToNext() val validTill = cursor.getString(0) @@ -332,7 +383,7 @@ return validTill } fun getValidTill(): String { - val cursor = db.rawQuery("select feed_end_date from feed_info", null) + val cursor = db!!.rawQuery("select feed_end_date from feed_info", null) cursor.moveToNext() val validTill = cursor.getString(0) @@ -341,41 +392,42 @@ cursor.close() return validTill } - fun getServiceForToday(): AgencyAndId { - val today = JCalendar.getInstance().get(JCalendar.DAY_OF_WEEK) + fun getServiceForToday(): String? { + val today = JCalendar.getInstance() return getServiceFor(today) } - fun getServiceForTomorrow(): AgencyAndId { + fun getServiceForTomorrow(): String? { val tomorrow = JCalendar.getInstance() tomorrow.add(JCalendar.DAY_OF_MONTH, 1) - val tomorrowDoW = tomorrow.get(JCalendar.DAY_OF_WEEK) - return getServiceFor(tomorrowDoW) + return getServiceFor(tomorrow) } - fun getServiceFor(day: Int): AgencyAndId { - val dayColumn = arrayOf("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")[((day + 5) % 7)] - val cursor = db.rawQuery("select service_id from calendar where $dayColumn = 1", null) + private fun getServiceFor(day: JCalendar): String? { + val dayColumn = arrayOf("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")[((day.get(JCalendar.DAY_OF_WEEK) + 5) % 7)] + val cursor = db!!.rawQuery("select service_id from calendar where $dayColumn = 1 and start_date < ? and ? < end_date", arrayOf(day.toIsoDate(), day.toIsoDate())) - val service: Int - cursor.moveToNext() - try { - service = cursor.getInt(0) + cursor.moveToFirst() + return try { + cursor.getInt(0).let { + cursor.close() + it.toString() + } + } catch (e: CursorIndexOutOfBoundsException) { cursor.close() - return AgencyAndId(service.toString()) - } catch (e: CursorIndexOutOfBoundsException) { - throw IllegalArgumentException() + null } } - fun getPlatesForStop(stop: AgencyAndId): Set<Plate.ID> { + private fun getPlatesForStop(stop: String): Set<Plate.ID> { + val plates = HashSet<Plate.ID>() - val cursor = db.rawQuery("select route_id, trip_headsign " + - "from stop_times natural join trips where stop_id = ? " + - "group by route_id, trip_headsign", arrayOf(stop.id)) + val cursor = db!!.rawQuery("select route_id, trip_headsign " + + "from stop_times natural join trips natural join stops where stop_code = ? " + + "group by route_id, trip_headsign", arrayOf(stop)) while (cursor.moveToNext()) { - val routeId = AgencyAndId(cursor.getString(0)) + val routeId = cursor.getString(0) val headsign = cursor.getString(1) plates.add(Plate.ID(routeId, stop, headsign)) } @@ -384,13 +436,13 @@ cursor.close() return plates } - fun getTripGraphs(id: AgencyAndId): Array<TripGraph> { + fun getTripGraphs(id: String): Array<TripGraph> { val graphs = arrayOf(TripGraph(), TripGraph()) - val cursor = db.rawQuery("select trip_id, trip_headsign, direction_id, stop_id, " + + val cursor = db!!.rawQuery("select trip_id, trip_headsign, direction_id, stop_id, " + "stop_sequence, pickup_type, stop_name, zone_id " + "from stop_times natural join trips natural join stops" + - "where route_id = ?", arrayOf(id.id)) + "where route_id = ?", arrayOf(id)) while (cursor.moveToNext()) { val trip = cursor.getString(0) @@ -435,6 +487,56 @@ } } return graphs + } + + fun getServiceFirstDay(service: String): Int { + val cursor = db!!.rawQuery("select * from calendar where service_id = ?", arrayOf(service)) + cursor.moveToFirst() + var i = 1 + while ((cursor.getString(i) == "0") and (i < 8)) i++ + cursor.close() + return i + } + + fun getServiceDescription(service: String, context: Context): String { + val dayNames = SparseArray<String>() + dayNames.put(1, context.getString(R.string.Mon)) + dayNames.put(2, context.getString(R.string.Tue)) + dayNames.put(3, context.getString(R.string.Wed)) + dayNames.put(4, context.getString(R.string.Thu)) + dayNames.put(5, context.getString(R.string.Fri)) + dayNames.put(6, context.getString(R.string.Sat)) + dayNames.put(7, context.getString(R.string.Sun)) + + val cursor = db!!.rawQuery("select * from calendar where service_id = ?", arrayOf(service)) + cursor.moveToFirst() + val days = SparseBooleanArray() + for (i in 1..7) { + days.append(i, cursor.getString(i) == "1") + } + days.append(8, false) + val description = ArrayList<String>() + var start = 0 + + for (i in 1..8) { + if (!days[i] and (start > 0)) { + when { + i - start == 1 -> description.add(dayNames[start]) + i - start == 2 -> description.add("${dayNames[start]}, ${dayNames[start + 1]}") + i - start > 2 -> description.add("${dayNames[start]}–${dayNames[i - 1]}") + } + start = 0 + } + if (days[i] and (start == 0)) + start = i + } + + val startDate = calendarFromIsoD(cursor.getString(8)).toNiceString(context) + val endDate = calendarFromIsoD(cursor.getString(9)).toNiceString(context) + + cursor.close() + + return "${description.joinToString { it }} ($startDate–$endDate)" } class TripGraph { diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt index 13a2f5765c5ec92fd13aa9ec8f05681c5f3ce0ed..c8915edf9e1b3e8b8444f3661e989ce6f3dc2810 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt @@ -16,7 +16,7 @@ import ml.adamsprogs.bimba.models.Departure import ml.adamsprogs.bimba.rollTime import java.util.* -class DeparturesAdapter(val context: Context, private val departures: List<Departure>?, private val relativeTime: Boolean) : +class DeparturesAdapter(val context: Context, var departures: List<Departure>?, var relativeTime: Boolean) : RecyclerView.Adapter<DeparturesAdapter.ViewHolder>() { companion object { @@ -26,31 +26,32 @@ const val VIEW_TYPE_EMPTY: Int = 2 } override fun getItemCount(): Int { - if (departures == null || departures.isEmpty()) + if (departures == null || departures!!.isEmpty()) return 1 - return departures.size + return departures!!.size } override fun getItemViewType(position: Int): Int { return when { - departures == null -> VIEW_TYPE_EMPTY - departures.isEmpty() -> VIEW_TYPE_LOADING + departures == null -> VIEW_TYPE_LOADING //empty + departures!!.isEmpty() -> VIEW_TYPE_LOADING else -> VIEW_TYPE_CONTENT } } override fun onBindViewHolder(holder: ViewHolder, position: Int) { + // todo migotanie ikon if (departures == null) { return } val line = holder.lineTextView val time = holder.timeTextView val direction = holder.directionTextView - if (departures.isEmpty()) { + if (departures!!.isEmpty()) { time.text = context.getString(R.string.no_departures) return } - val departure = departures[position] + val departure = departures!![position] val now = Calendar.getInstance() val departureTime = Calendar.getInstance().rollTime(departure.time) if (departure.tomorrow) diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt index 6814ada2b4f7f4e9ff219337c1a4e1e03c643fae..d9a29e8872e4a7f99c68c282ef20cdf823791fb4 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt @@ -6,29 +6,60 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import kotlinx.coroutines.experimental.DefaultDispatcher +import kotlinx.coroutines.experimental.android.UI +import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.withContext +import ml.adamsprogs.bimba.ProviderProxy import ml.adamsprogs.bimba.R import ml.adamsprogs.bimba.collections.FavouriteStorage import ml.adamsprogs.bimba.models.Favourite import ml.adamsprogs.bimba.models.Plate -import ml.adamsprogs.bimba.models.Timetable +import ml.adamsprogs.bimba.models.StopSegment + class FavouriteEditRowAdapter(private var favourite: Favourite) : RecyclerView.Adapter<FavouriteEditRowAdapter.ViewHolder>() { + + private val segments = HashMap<String, StopSegment>() + private val providerProxy = ProviderProxy() + + init { + launch(UI) { + withContext(DefaultDispatcher) { + favourite.segments.forEach { + segments[it.stop] = providerProxy.fillStopSegment(it) ?: it + } + } + this@FavouriteEditRowAdapter.notifyDataSetChanged() + } + } + + override fun getItemCount(): Int { - return favourite.size + return segments.flatMap { it.value.plates ?: emptyList<Plate.ID>() }.size } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val timetable = Timetable.getTimetable() - val favourites = FavouriteStorage.getFavouriteStorage() - val id = favourite.segments.flatMap { it.plates!! }.sortedBy { "${it.line}${it.stop}"}[position] - val plate = Plate(id, null) - val favouriteElement = "${timetable.getStopName(plate.id.stop)} ( ${timetable.getStopCode(plate.id.stop)}):\n${plate.id.line} → ${plate.id.headsign}" - holder.rowTextView.text = favouriteElement - holder.deleteButton.setOnClickListener { - favourites.delete(favourite.name, id) - favourite = favourites.favourites[favourite.name]!! - notifyDataSetChanged() + launch(UI) { + val plates = segments.flatMap { it.value.plates ?: emptyList<Plate.ID>() } + val favourites = FavouriteStorage.getFavouriteStorage() + val id = plates.sortedBy { "${it.line}${it.stop}" }[position] + val favouriteElement = withContext(DefaultDispatcher) { + providerProxy.getStopName(id.stop).let { + "${it ?: ""} (${id.stop}):\n${id.line} → ${id.headsign}" + } + } + holder.rowTextView.text = favouriteElement + holder.deleteButton.setOnClickListener { + launch(UI) { + favourite.segments.clear() + favourite.segments.addAll(segments.map { it.value }) + favourites.delete(favourite.name, id) + favourite = favourites.favourites[favourite.name]!! + notifyDataSetChanged() + } + } } } @@ -41,7 +72,7 @@ return ViewHolder(rowView) } inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val rowTextView:TextView = itemView.findViewById(R.id.favourite_edit_row) - val deleteButton:ImageView = itemView.findViewById(R.id.favourite_edit_delete) + val rowTextView: TextView = itemView.findViewById(R.id.favourite_edit_row) + val deleteButton: ImageView = itemView.findViewById(R.id.favourite_edit_delete) } } \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt index 6cef0867376e4bcf1b8aecf4df91fc3b683ede7f..76206a2b5e93f9683ff8dc07aa7011fa48c2a29e 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt @@ -9,10 +9,8 @@ import android.view.* import android.widget.* import ml.adamsprogs.bimba.R import android.view.LayoutInflater -import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.android.UI -import kotlinx.coroutines.experimental.async -import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.* import java.util.* import ml.adamsprogs.bimba.Declinator import ml.adamsprogs.bimba.collections.FavouriteStorage @@ -61,7 +59,7 @@ val favourite = favourites[position]!! holder.nameTextView.text = favourite.name holder.selectedOverlay.visibility = if (isSelected(position)) View.VISIBLE else View.INVISIBLE - holder.moreButton.setOnClickListener { + holder.moreButton.setOnClickListener { it -> val popup = PopupMenu(appContext, it) val inflater = popup.menuInflater popup.setOnMenuItemClickListener { @@ -75,9 +73,9 @@ inflater.inflate(R.menu.favourite_actions, popup.menu) popup.show() } - val nextDeparture = async(CommonPool) { + val nextDeparture = withContext(CommonPool) { favourite.nextDeparture() - }.await() + } val nextDepartureText: String val nextDepartureLineText: String diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/ServiceAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/ServiceAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..014cd4a7cf014ff56ec0ca3e54a77c90a769f805 --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/ServiceAdapter.kt @@ -0,0 +1,43 @@ +package ml.adamsprogs.bimba.models.adapters + +import android.annotation.SuppressLint +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.view.LayoutInflater +import ml.adamsprogs.bimba.R +import android.app.Activity +import android.widget.ArrayAdapter +import ml.adamsprogs.bimba.ProviderProxy + + +class ServiceAdapter(context: Activity, resourceId: Int, list: List<RowItem>) : ArrayAdapter<ServiceAdapter.RowItem>(context, resourceId, list) { + + private val inflater: LayoutInflater = context.layoutInflater + + @SuppressLint("ViewHolder", "InflateParams") + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val rowItem: RowItem = getItem(position) + val rowView = inflater.inflate(R.layout.toolbar_spinner_item, null, true) + rowView.findViewById<TextView>(R.id.text).text = rowItem.description + + return rowView + } + + @SuppressLint("InflateParams") + override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup?): View { + val rowItem: RowItem = getItem(position) + val rowView = inflater.inflate(R.layout.toolbar_spinner_item, null, true) + rowView.findViewById<TextView>(R.id.text).text = rowItem.description + + return rowView + + } + + data class RowItem(val service: String, val description: String) : Comparable<RowItem> { + override fun compareTo(other: RowItem): Int { + val proxy = ProviderProxy() + return proxy.getServiceFirstDay(service).compareTo(proxy.getServiceFirstDay(other.service)) + } + } +} diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/AgencyAndId.kt b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/AgencyAndId.kt deleted file mode 100644 index 66c8d620d3dfbcd85c3def1d14d5cfee300e878e..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/AgencyAndId.kt +++ /dev/null @@ -1,19 +0,0 @@ -package ml.adamsprogs.bimba.models.gtfs - -import java.io.Serializable - -data class AgencyAndId(val id: String) : Serializable, Comparable<AgencyAndId> { - override fun compareTo(other: AgencyAndId): Int { - return this.toString().compareTo(other.toString()) - } - - companion object { - fun convertFromString(str: String): AgencyAndId { - return AgencyAndId(str) - } - } - - override fun toString(): String { - return id - } -} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Route.kt b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Route.kt index ddba5daf28fd737a6c067c263d10c19ecc9d05d0..c3021d8a3c0991b4cee3150a5a2f9751a2ea8764 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Route.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Route.kt @@ -4,7 +4,7 @@ import android.os.Parcel import android.os.Parcelable -data class Route(val id: AgencyAndId, val agency: AgencyAndId, val shortName: String, +data class Route(val id: String, val agency: String, val shortName: String, val longName: String, val description: String, val type: Int, val colour: Int, val textColour: Int, val modifications: Map<String, String>) : Parcelable { companion object CREATOR : Parcelable.Creator<Route> { @@ -27,13 +27,13 @@ val fromSplit = from.split("^") val toSplit = to.split("^") val description = "${toSplit[0]}|${fromSplit[0]}" val modifications = createModifications(desc) - Route(AgencyAndId(id), AgencyAndId(agency), shortName, longName, description, + Route(id, agency, shortName, longName, description, type, colour, textColour, modifications) } else { val toSplit = desc.split("^") val description = toSplit[0] val modifications = createModifications(desc) - Route(AgencyAndId(id), AgencyAndId(agency), shortName, longName, description, + Route(id, agency, shortName, longName, description, type, colour, textColour, modifications) } } @@ -57,8 +57,8 @@ } @Suppress("UNCHECKED_CAST") constructor(parcel: Parcel) : this( - AgencyAndId(parcel.readString()), - AgencyAndId(parcel.readString()), + parcel.readString(), + parcel.readString(), parcel.readString(), parcel.readString(), parcel.readString(), @@ -68,8 +68,8 @@ parcel.readInt(), parcel.readSerializable() as HashMap<String, String>) override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(id.id) - parcel.writeString(agency.id) + parcel.writeString(id) + parcel.writeString(agency) parcel.writeString(shortName) parcel.writeString(longName) parcel.writeString(description) diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Trip.kt b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Trip.kt index 6e2de9c4c35d16fd71e0a31745ee277dc5947134..db797a31de4ba22531ebe14266ed73ed38e72254 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Trip.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Trip.kt @@ -1,9 +1,9 @@ package ml.adamsprogs.bimba.models.gtfs -data class Trip(val routeId: AgencyAndId, val serviceId: AgencyAndId, val id: ID, - val headsign: String, val direction: Int, val shapeId: AgencyAndId, +data class Trip(val routeId: String, val serviceId: String, val id: ID, + val headsign: String, val direction: Int, val shapeId: String, val wheelchairAccessible: Boolean) { - data class ID(val rawId:String, val id: AgencyAndId, val modification: Set<Modification>, val isMain: Boolean) { - data class Modification(val id: AgencyAndId, val stopRange: IntRange?) + data class ID(val rawId:String, val id: String, val modification: Set<Modification>, val isMain: Boolean) { + data class Modification(val id: String, val stopRange: IntRange?) } } \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/suggestions/StopSuggestion.kt b/app/src/main/java/ml/adamsprogs/bimba/models/suggestions/StopSuggestion.kt index 934a6e0e2ef25631fe6fde8162becd1260b8a8d8..cd3ef7762cb9f0e16a905cba2df8193e9d8b4568 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/suggestions/StopSuggestion.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/suggestions/StopSuggestion.kt @@ -3,11 +3,10 @@ import android.os.Parcel import android.os.Parcelable import ml.adamsprogs.bimba.R -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId -class StopSuggestion(name: String, val ids: Set<AgencyAndId>, private val zone: String, private val zoneColour: String) : GtfsSuggestion(name){ +class StopSuggestion(name: String, private val zone: String, private val zoneColour: String) : GtfsSuggestion(name){ @Suppress("UNCHECKED_CAST") - constructor(parcel: Parcel) : this(parcel.readString(), parcel.readString().split(",").map { AgencyAndId(it) }.toSet(), parcel.readString(), parcel.readString()) + constructor(parcel: Parcel) : this(parcel.readString(), parcel.readString(), parcel.readString()) override fun describeContents(): Int { return Parcelable.CONTENTS_FILE_DESCRIPTOR @@ -15,7 +14,6 @@ } override fun writeToParcel(dest: Parcel?, flags: Int) { dest?.writeString(name) - dest?.writeString(ids.joinToString(",") { it.toString() }) dest?.writeString(zone) dest?.writeString(zoneColour) } diff --git a/app/src/main/res/drawable/icon_dev.xml b/app/src/main/res/drawable/icon_dev.xml new file mode 100644 index 0000000000000000000000000000000000000000..a4b238053d6fdefaa6c74cfe50a206f56b769aff --- /dev/null +++ b/app/src/main/res/drawable/icon_dev.xml @@ -0,0 +1,17 @@ +<vector android:height="24dp" android:viewportHeight="293" + android:viewportWidth="298" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillAlpha="1" android:fillColor="#5E5E5E" + android:pathData="m62.925,292.677v0c-3.69,-0.897 -5.684,-3.789 -4.288,-6.482L90.846,223.472c1.296,-2.593 5.485,-3.989 9.174,-2.992v0c3.69,0.897 5.684,3.789 4.288,6.482l-32.209,62.723c-1.296,2.593 -5.385,3.989 -9.174,2.992z" android:strokeWidth="0.99718279"/> + <path android:fillAlpha="1" android:fillColor="#5E5E5E" + android:pathData="m229.156,292.677v0c3.69,-0.897 5.684,-3.789 4.288,-6.482l-32.209,-62.723c-1.296,-2.593 -5.485,-3.989 -9.174,-2.992v0c-3.69,0.897 -5.684,3.789 -4.288,6.482l32.209,62.723c1.296,2.593 5.485,3.989 9.174,2.992z" android:strokeWidth="0.99718279"/> + <path android:fillAlpha="1" android:fillColor="#5E5E5E" + android:pathData="m151.076,37.797 l-0.598,0.199c-1.795,0.698 -3.789,-0.299 -4.487,-2.094L136.418,9.277c-0.698,-1.795 0.299,-3.789 2.094,-4.487l0.598,-0.199c1.795,-0.698 3.789,0.299 4.487,2.094l9.573,26.625c0.698,1.895 -0.299,3.889 -2.094,4.487z" android:strokeWidth="0.99718279"/> + <path android:fillAlpha="1" android:fillColor="#5E5E5E" + android:pathData="m180.294,4.79v0c0,2.094 -1.695,3.789 -3.789,3.789h-60.828c-2.094,0 -3.789,-1.695 -3.789,-3.789v0c0,-2.094 1.695,-3.789 3.789,-3.789h60.828c2.094,0 3.789,1.695 3.789,3.789z" android:strokeWidth="0.99718279"/> + <path android:fillAlpha="1" android:fillColor="#5E5E5E" + android:pathData="M218.286,237.034H73.795c-13.263,0 -23.932,-10.77 -23.932,-23.932V105.406c0,-41.184 33.406,-74.689 74.689,-74.689h43.178c41.184,0 74.689,33.406 74.689,74.689v107.696c-0.1,13.163 -10.869,23.932 -24.132,23.932z" android:strokeWidth="0.99718279"/> + <path android:fillColor="#FFFFFF" android:pathData="M212.104,146.789L79.977,146.789c-5.584,0 -10.171,-4.487 -10.171,-10.171l0,-34.802c0,-16.852 13.661,-30.514 30.514,-30.514L191.761,71.302c16.852,0 30.514,13.661 30.514,30.514l0,34.802c-0.1,5.684 -4.587,10.171 -10.171,10.171z"/> + <path android:fillColor="#FFFFFF" android:pathData="M161.148,56.344L130.933,56.344c-3.191,0 -5.684,-2.593 -5.684,-5.684l0,0c0,-3.191 2.593,-5.684 5.684,-5.684l30.215,0c3.191,0 5.684,2.593 5.684,5.684l0,0c0,3.091 -2.593,5.684 -5.684,5.684z"/> + <path android:fillColor="#FFFFFF" android:pathData="M86.957,192.36m-14.758,0a14.758,14.758 0,1 1,29.517 0a14.758,14.758 0,1 1,-29.517 0"/> + <path android:fillColor="#FFFFFF" android:pathData="M205.223,192.36m-14.758,0a14.758,14.758 0,1 1,29.517 0a14.758,14.758 0,1 1,-29.517 0"/> +</vector> diff --git a/app/src/main/res/layout/activity_stop.xml b/app/src/main/res/layout/activity_stop.xml index cad4b10f71491458bd9cc388e9db098c409a41da..d6a0b92bfbdd7ad1d7be8b4b6481ca929b573770 100644 --- a/app/src/main/res/layout/activity_stop.xml +++ b/app/src/main/res/layout/activity_stop.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" +<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/stop_layout" @@ -7,6 +7,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:context="ml.adamsprogs.bimba.activities.StopActivity"> + <android.support.design.widget.AppBarLayout android:id="@+id/appbar" @@ -27,36 +28,29 @@ app:title="@string/app_name"> </android.support.v7.widget.Toolbar> - <android.support.design.widget.TabLayout - android:id="@+id/tabs" + <Spinner + android:id="@+id/dateSpinner" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:layout_marginBottom="8dp" + android:layout_marginEnd="8dp" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + android:layout_marginStart="8dp" + android:layout_weight="1" + android:visibility="gone" /> - <android.support.design.widget.TabItem - android:id="@+id/tabItem" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/tab_workday_text" /> - - <android.support.design.widget.TabItem - android:id="@+id/tabItem4" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/tab_saturday_text" /> - <android.support.design.widget.TabItem - android:id="@+id/tabItem5" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/tab_sunday_text" /> - </android.support.design.widget.TabLayout> </android.support.design.widget.AppBarLayout> - <android.support.v4.view.ViewPager - android:id="@+id/container" - android:layout_width="match_parent" - android:layout_height="match_parent" - app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + <android.support.v7.widget.RecyclerView + android:id="@+id/departuresList" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/appbar" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" @@ -64,6 +58,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end|bottom" android:layout_margin="@dimen/fab_margin" + android:layout_marginBottom="16dp" + android:layout_marginEnd="16dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" app:srcCompat="@drawable/ic_favourite" /> - -</android.support.design.widget.CoordinatorLayout> +</android.support.constraint.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_stop.xml b/app/src/main/res/layout/fragment_stop.xml deleted file mode 100644 index 4cadd826bbf25470cfb9f7417b425acadc0c9722..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/fragment_stop.xml +++ /dev/null @@ -1,24 +0,0 @@ -<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:id="@+id/constraintLayout" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context="ml.adamsprogs.bimba.activities.StopActivity$PlaceholderFragment"> - - <!-- todo landscape version --> - <android.support.v7.widget.RecyclerView - android:id="@+id/departuresList" - android:layout_width="368dp" - android:layout_height="516dp" - android:layout_marginBottom="8dp" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.485" - app:layout_constraintEnd_toEndOf="parent" - android:layout_marginEnd="8dp" /> - -</android.support.constraint.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_spinner_item.xml b/app/src/main/res/layout/toolbar_spinner_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..1cb3890d88df2edaeec0cf238d7d7afb271c3f9c --- /dev/null +++ b/app/src/main/res/layout/toolbar_spinner_item.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:singleLine="true" + android:textAlignment="inherit" + android:textColor="@color/text_on_toolbar" /> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9c139ca27b5819a8a45720dac6414ea95bab8fd7..5982e32ecdb6a838a1c7f45e3f82e9a77241302e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -12,4 +12,5 @@ #ffc107 <color name="tram">#00adef</color> <color name="bus">#c4212a</color> + <color name="text_on_toolbar">#ffffff</color> </resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 59bae428d441c18d3f8b73074d7e738ee2d1c448..fc6cb018421f5b01bd650be84078c6b1522df076 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -90,4 +90,12 @@key_timetable_source_url <string name="title_timetable_source_url">Timetable source</string> <string name="title_activity_settings">Settings</string> <string name="settings">Settings</string> + + <string name="Mon">Mon</string> + <string name="Tue">Tue</string> + <string name="Wed">Wed</string> + <string name="Thu">Thu</string> + <string name="Fri">Fri</string> + <string name="Sat">Sat</string> + <string name="Sun">Sun</string> </resources> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 74a1403bc6cf2643facca4506c5f1b23cd015a27..bc754c49306b35a193dcb649178a347de04d8599 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -73,4 +73,11 @@Quelle des Fahrplans <string name="title_activity_settings">Einstellungen</string> <string name="settings">Einstellungen</string> <string name="timetable_decompressing">Fahrplan wird entpackt</string> + <string name="Mon">Mo.</string> + <string name="Tue">Di.</string> + <string name="Wed">Mi.</string> + <string name="Thu">Do.</string> + <string name="Fri">Fr.</string> + <string name="Sat">Sa.</string> + <string name="Sun">So.</string> </resources> diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e18d6d65bf573b10800637ce426adbf393ea9bda..2f555258b4a51b199fde26df685ba07788e5bc20 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -71,4 +71,11 @@Fonte dell’Orario <string name="title_activity_settings">Impostazioni</string> <string name="settings">Impostazioni</string> <string name="timetable_decompressing">Decomprimendo l’orario</string> + <string name="Mon">lun</string> + <string name="Tue">mar</string> + <string name="Wed">mer</string> + <string name="Thu">gio</string> + <string name="Fri">ven</string> + <string name="Sat">sab</string> + <string name="Sun">dom</string> </resources> diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3693fadac99d212054a413ec878bf37f093d5c77..82150b353f9a2b6c74446ff0d4415c6b8b4a12db 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -74,4 +74,11 @@Bron van de dienstregeling <string name="title_activity_settings">Instellingen</string> <string name="settings">Instellingen</string> <string name="timetable_decompressing">Bezig met uitpakken van dienstregeling</string> + <string name="Mon">ma</string> + <string name="Tue">di</string> + <string name="Wed">wo</string> + <string name="Thu">do</string> + <string name="Fri">vr</string> + <string name="Sat">za</string> + <string name="Sun">zo</string> </resources> diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 15370f10365138c4426f4c71c7ab5af1bc7370b0..ce9634e0554f906aa47db97f48b47415abfe3742 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -73,4 +73,11 @@Źródło rozkładu <string name="title_activity_settings">Ustawienia</string> <string name="settings">Ustawienia</string> <string name="timetable_decompressing">Rozpakowywanie rozkładu</string> + <string name="Mon">pon.</string> + <string name="Tue">wt.</string> + <string name="Wed">śr.</string> + <string name="Thu">czw.</string> + <string name="Fri">pt.</string> + <string name="Sat">sob.</string> + <string name="Sun">niedz.</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml index 1c3647edb4ef835066979a1174d44dbd79359557..edbdd769a5229ecc162ac8bc3b128c86a91dbe4d 100644 --- a/app/src/main/res/xml/pref_main.xml +++ b/app/src/main/res/xml/pref_main.xml @@ -9,5 +9,10 @@ android:title="@string/title_timetable_source_url" /> <!-- todo intent get file (import) --> <!-- todo reset source --> + <SwitchPreference + android:defaultValue="false" + android:key="key_timetable_automatic_update" + android:summary="Automatically check for and download timetable updates" + android:title="Automatic updates" /> </PreferenceCategory> </PreferenceScreen> diff --git a/build.gradle b/build.gradle index abd71c41b0e3809a905d7e36ce37c4c9b60fb0b2..eb4b96ee435f6e9def2abe971a838aba0ac9b430 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.2.51' + ext.kotlin_version = '1.2.60' repositories { jcenter() maven { url 'https://maven.google.com' } @@ -9,7 +9,7 @@ //maven { url 'https://dl.bintray.com/guardian/android' } // TooLargeTool google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.3' + classpath 'com.android.tools.build:gradle:3.1.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/converter/local/converter.py b/converter/local/converter.py index 8c817c00e9c0a965d79f605e37d95f2f8d3a94fc..842f7e79adce9f07473c6401dd332bb751cf1dff 100755 --- a/converter/local/converter.py +++ b/converter/local/converter.py @@ -87,6 +87,7 @@ s, e = name.split('_') return s + "{0:03}".format(100 - self.__validity_length(s, e)) def __clean_overlapping(self, names): + today = date.today().strftime('%Y%m%d') names.sort(key=self.__sort_key) print(names) if len(names) == 1: @@ -96,7 +97,8 @@ i = 1 for name in names[1:]: this_start, this_end = name.split('_') prev_start, prev_end = names[i-1].split('_') - if not (this_start < prev_end or this_start == prev_start): + if not ((this_start < prev_end and this_start <= today) + or this_start == prev_start): return_names.append(names[i-1]) i = i + 1 diff --git a/converter/server/upload.php b/converter/server/upload.php index d95574fc8c3975c3a70054c8bab8f5cde9bee938..6b9e7e6b4bbbfcc9483f778ca956d5d2a0b31b81 100644 --- a/converter/server/upload.php +++ b/converter/server/upload.php @@ -37,7 +37,13 @@ foreach ($timetables as $id => $timetable) { $t = $timetable['t']; $sha = $timetable['sha']; - // todo if $id in $oldMetadata -> skip + $shallSkip = false; + foreach ($oldMetadata as $entry) { + if ($entry['id'] == $id) + $shallSkip = true; + } + + if ($shallSkip) continue; $fp = fopen(dirname(__FILE__) . "/$id.db.gz", 'wb'); $ch = curl_init($t);