Author: Adam Pioterek <adam.pioterek@protonmail.ch>
scheduleless in stops
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/build.gradle b/app/build.gradle index d20a839f1360318e2dce6b81ef8d8c2fdb0a44aa..50d4623bad91c9ccaeac333cac6c4e2a930a7e6e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 27 - buildToolsVersion "28.0.1" + buildToolsVersion "28.0.2" defaultConfig { applicationId "ml.adamsprogs.bimba.scheduleless" minSdkVersion 19 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 40d8d758b7391a5f9ee273336c2185a987c9c309..249294c52d3e85f02f2c65f0a8458e1e26631220 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,7 +44,7 @@ android:label="@string/title_activity_help" android:theme="@style/AppTheme" /> <service - android:name=".datasources.VmClient" + android:name=".datasources.VmService" android:enabled="true" android:exported="false" /> diff --git a/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt b/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt index 9668aa001bce218cf2f16f22b1e70a19b4a8ff75..81c6bc9779bdbb6cfa7ef4faa87f43af010795a1 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/MessageReceiver.kt @@ -4,7 +4,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import ml.adamsprogs.bimba.datasources.TimetableDownloader -import ml.adamsprogs.bimba.datasources.VmClient +import ml.adamsprogs.bimba.datasources.VmService import ml.adamsprogs.bimba.models.Departure import ml.adamsprogs.bimba.models.Plate @@ -28,11 +28,12 @@ for (listener in onTimetableDownloadListeners) { listener.onTimetableDownload(result) } } - if (intent?.action == VmClient.ACTION_READY) { - val departures = intent.getStringArrayListExtra(VmClient.EXTRA_DEPARTURES)?.map { Departure.fromString(it) }?.toSet() - val plateId = intent.getSerializableExtra(VmClient.EXTRA_PLATE_ID) as Plate.ID + if (intent?.action == VmService.ACTION_READY) { + val departures = intent.getStringArrayListExtra(VmService.EXTRA_DEPARTURES)?.map { Departure.fromString(it) }?.toSet() + val plateId = intent.getSerializableExtra(VmService.EXTRA_PLATE_ID) as Plate.ID + val stopCode = intent.getSerializableExtra(VmService.EXTRA_STOP_CODE) as String for (listener in onVmListeners) { - listener.onVm(departures, plateId) + listener.onVm(departures, plateId, stopCode) } } } @@ -58,6 +59,6 @@ fun onTimetableDownload(result: String?) } interface OnVmListener { - fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID) + fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID, stopCode: String) } } \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt b/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt index 80a47f9442b8b6dec1b40f7acdbf16ad2e34d59e..0dcddcffc601cb437e27a01a15286715735d045b 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/ProviderProxy.kt @@ -1,16 +1,27 @@ package ml.adamsprogs.bimba -import android.content.Context +import android.annotation.SuppressLint +import android.content.* import kotlinx.coroutines.experimental.android.UI import kotlinx.coroutines.experimental.* -import ml.adamsprogs.bimba.datasources.VmStopsClient -import ml.adamsprogs.bimba.models.Timetable +import ml.adamsprogs.bimba.activities.StopActivity +import ml.adamsprogs.bimba.datasources.* +import ml.adamsprogs.bimba.models.* import ml.adamsprogs.bimba.models.suggestions.* +import java.util.* -class ProviderProxy(context: Context) { - private val vmStopsClient = VmStopsClient.getVmStopClient() - private val timetable: Timetable = Timetable.getTimetable(context) +//todo make singleton +class ProviderProxy(context: Context? = null) { + private val vmStopsClient = VmClient.getVmStopClient() + private var timetable: Timetable = Timetable.getTimetable(context) private var suggestions = emptyList<GtfsSuggestion>() + private val requests = HashMap<String, Request>() + var mode = if (timetable.isEmpty()) MODE_VM else MODE_FULL + + companion object { + const val MODE_FULL = "mode_full" + const val MODE_VM = "mode_vm" + } fun getSuggestions(query: String = "", callback: (List<GtfsSuggestion>) -> Unit) { launch(UI) { @@ -65,6 +76,120 @@ vmSheds } callback(sheds) + } + } + + fun subscribeForDepartures(stopSegments: Set<StopSegment>, listener: OnDeparturesReadyListener, context: Context): String { + stopSegments.forEach { + val intent = Intent(context, VmService::class.java) + intent.putExtra("stop", it.stop) + intent.action = "request" + context.startService(intent) + } + val uuid = UUID.randomUUID().toString() + requests[uuid] = Request(listener, stopSegments) + return uuid + } + + fun subscribeForDepartures(stopCode: String, listener: StopActivity, context: StopActivity): String { + val intent = Intent(context, VmService::class.java) + intent.putExtra("stop", stopCode) + intent.action = "request" + context.startService(intent) + + val uuid = UUID.randomUUID().toString() + requests[uuid] = Request(listener, setOf(StopSegment(stopCode, null))) + return uuid + } + + private fun constructSegmentDepartures(stopSegments: Set<StopSegment>): Set<Departure> { + if (timetable.isEmpty()) + return emptySet() + else + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + fun unsubscribeFromDepartures(uuid: String, context: Context) { + requests[uuid]?.unsubscribe(context) + requests.remove(uuid) + } + + fun refreshTimetable(context: Context) { + timetable = Timetable.getTimetable(context, true) + mode = MODE_FULL + } + + fun getFullTimetable(stopCode: String): Map<Int, List<Departure>> { + val departures = if (timetable.isEmpty()) + emptyMap() + else + timetable.getStopDepartures(stopCode) + + return convertCalendarModes(departures) + } + + fun getFullTimetable(stopSegments: Set<StopSegment>): Map<Int, List<Departure>> { + val departures = if (timetable.isEmpty()) + emptyMap() + else + timetable.getStopDeparturesBySegments(stopSegments) + + return convertCalendarModes(departures) + } + + @SuppressLint("UseSparseArrays") + private fun convertCalendarModes(raw: Map<String, List<Departure>>): Map<Int, List<Departure>> { + val sunday = timetable.getServiceFor(Calendar.SUNDAY) + val saturday = timetable.getServiceFor(Calendar.SATURDAY) + + val departures = HashMap<Int, List<Departure>>() + departures[StopActivity.MODE_WORKDAYS] = + try { + raw.filter { it.key != saturday && it.key != sunday }.toList()[0].second + } catch (e: IndexOutOfBoundsException) { + ArrayList<Departure>() + } + + departures[StopActivity.MODE_SATURDAYS] = raw[saturday] ?: ArrayList() + departures[StopActivity.MODE_SUNDAYS] = raw[sunday] ?: ArrayList() + + return departures + } + + interface OnDeparturesReadyListener { + fun onDeparturesReady(departures: Set<Departure>, plateId: Plate.ID) + } + + inner class Request(private val listener: OnDeparturesReadyListener, private val segments: Set<StopSegment>) : MessageReceiver.OnVmListener { + private val receiver = MessageReceiver.getMessageReceiver() + private val receivedPlates = HashSet<Plate.ID>() + + init { + receiver.addOnVmListener(this) + } + + override fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID, stopCode: String) { + if (segments.any { plateId in it }) { + if (vmDepartures != null) { + listener.onDeparturesReady(vmDepartures, plateId) + if (plateId !in receivedPlates) + receivedPlates.add(plateId) + } else { + receivedPlates.remove(plateId) + if (receivedPlates.isEmpty()) + listener.onDeparturesReady(constructSegmentDepartures(segments), plateId) + } + } + } + + fun unsubscribe(context: Context) { + segments.forEach { + val intent = Intent(context, VmService::class.java) + intent.putExtra("stop", it.stop) + intent.action = "remove" + context.startService(intent) + } + receiver.removeOnVmListener(this) } } } \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt index 0c1c1c85ec7488adba5ff3146321fc3397148d18..35d8e4cf583bf4dc49428fff87ec963d663ce135 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt @@ -29,8 +29,8 @@ import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion //todo<p:1> searchView integration class DashActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, - FavouritesAdapter.OnMenuItemClickListener, Favourite.OnVmPreparedListener, - FavouritesAdapter.ViewHolder.OnClickListener { + FavouritesAdapter.OnMenuItemClickListener, FavouritesAdapter.ViewHolder.OnClickListener, ProviderProxy.OnDeparturesReadyListener { + val context: Context = this private val receiver = MessageReceiver.getMessageReceiver() private lateinit var timetable: Timetable @@ -45,6 +45,7 @@ private val actionModeCallback = ActionModeCallback() private var actionMode: ActionMode? = null private var isWarned = false private lateinit var providerProxy: ProviderProxy + private val listenersIds = HashSet<String>() companion object { const val REQUEST_EDIT_FAVOURITE = 1 @@ -224,7 +225,7 @@ private fun prepareFavourites() { favourites = FavouriteStorage.getFavouriteStorage(context) favourites.forEach { - it.addOnVmPreparedListener(this) + listenersIds.add(it.subscribeForDepartures(this, this)) } val layoutManager = LinearLayoutManager(context) favouritesList = favourites_list @@ -234,7 +235,7 @@ favouritesList.itemAnimator = DefaultItemAnimator() favouritesList.layoutManager = layoutManager } - override fun onVmPrepared() { + override fun onDeparturesReady(departures: Set<Departure>, plateId: Plate.ID) { favouritesList.adapter.notifyDataSetChanged() } @@ -246,11 +247,10 @@ } private fun prepareListeners() { val filter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED) - filter.addAction(VmClient.ACTION_READY) + filter.addAction(VmService.ACTION_READY) filter.addCategory(Intent.CATEGORY_DEFAULT) registerReceiver(receiver, filter) receiver.addOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener) - favourites.registerOnVm(receiver, context) } private fun startDownloaderService() { @@ -258,7 +258,7 @@ if (getDefaultSharedPreferences(this).getBoolean("automatic timetable updates", false)) startService(Intent(context, TimetableDownloader::class.java)) } - override fun onBackPressed() { + override fun onBackPressed() { //fixme if (drawerLayout.isDrawerOpen(drawerView)) { drawerLayout.closeDrawer(drawerView) return @@ -276,8 +276,8 @@ } override fun onDestroy() { super.onDestroy() + listenersIds.forEach { providerProxy.unsubscribeFromDepartures(it, this) } receiver.removeOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener) - favourites.deregisterOnVm(receiver, context) unregisterReceiver(receiver) } diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/LineSpecifyActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/LineSpecifyActivity.kt index 5d7d949f83b9112fd606741d84d156d57990cd8a..e33d1edd5cf5253071bf0936ca77d35211ce2426 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/LineSpecifyActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/LineSpecifyActivity.kt @@ -30,7 +30,7 @@ val line = intent.getStringExtra(EXTRA_LINE_ID) val timetable = Timetable.getTimetable() - val graphs = timetable.getTripGraphs(AgencyAndId(line)) + val graphs = timetable.getTripGraphs(line) sectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager, graphs) diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt index 18d6938b4f6cf5b79ed02117238c287f9ec3ecc9..e9e1c623ac6479d893b707dd64b4fc7bf1314e99 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt @@ -15,17 +15,16 @@ import kotlinx.android.synthetic.main.activity_stop.* import ml.adamsprogs.bimba.* import ml.adamsprogs.bimba.collections.FavouriteStorage import ml.adamsprogs.bimba.datasources.* -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId import ml.adamsprogs.bimba.models.* import ml.adamsprogs.bimba.models.adapters.DeparturesAdapter -class StopActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, MessageReceiver.OnVmListener, Favourite.OnVmPreparedListener { +class StopActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, ProviderProxy.OnDeparturesReadyListener { private var sectionsPagerAdapter: SectionsPagerAdapter? = null companion object { - const val EXTRA_STOP_ID = "stopId" const val EXTRA_STOP_CODE = "stopCode" + const val EXTRA_STOP_NAME = "stopName" const val EXTRA_FAVOURITE = "favourite" const val SOURCE_TYPE = "sourceType" const val SOURCE_TYPE_STOP = "stop" @@ -34,50 +33,47 @@ const val MODE_WORKDAYS = 0 const val MODE_SATURDAYS = 1 const val MODE_SUNDAYS = 2 + + const val TIMETABLE_TYPE_DEPARTURE = "timetable_type_departure" + const val TIMETABLE_TYPE_FULL = "timetable_type_full" } - private var stopSegment: StopSegment? = null + private var stopCode = "" private var favourite: Favourite? = null private var timetableType = "departure" - private lateinit var timetable: Timetable private val context = this private val receiver = MessageReceiver.getMessageReceiver() - private val vmDepartures = HashMap<Plate.ID, Set<Departure>>() - private var hasDepartures = false - private var lastUpdated = 0L + private lateinit var providerProxy: ProviderProxy + private val departures = HashMap<Plate.ID, Set<Departure>>() + private val fullDepartures = HashMap<Int, List<Departure>>() + private lateinit var subscriptionId: String + private lateinit var sourceType: String override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_stop) - timetable = Timetable.getTimetable(this) + providerProxy = ProviderProxy(this) sourceType = intent.getStringExtra(SOURCE_TYPE) setSupportActionBar(toolbar) - val departures = when (sourceType) { + when (sourceType) { SOURCE_TYPE_STOP -> { - stopSegment = StopSegment(intent.getSerializableExtra(EXTRA_STOP_ID) as AgencyAndId, null).apply { fillPlates() } - supportActionBar?.title = timetable.getStopName(stopSegment!!.stop) - null + stopCode = intent.getSerializableExtra(EXTRA_STOP_CODE) as String + supportActionBar?.title = intent.getSerializableExtra(EXTRA_STOP_NAME) as String } SOURCE_TYPE_FAV -> { favourite = intent.getParcelableExtra(EXTRA_FAVOURITE) supportActionBar?.title = favourite!!.name - favourite!!.addOnVmPreparedListener(this) - if (favourite!!.fullDepartures.isNotEmpty()) - favourite!!.fullDepartures - else - null } - else -> null } showFab() - sectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager, departures) + sectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager, null) container.adapter = sectionsPagerAdapter @@ -87,51 +83,20 @@ selectTodayPage() prepareOnDownloadListener() - } - - private fun getFavouriteDepartures() { - refreshAdapter(favourite!!.allDepartures()) - } - - private fun refreshAdapterFromStop() { - val now = Calendar.getInstance().secondsAfterMidnight() - val departures = HashMap<AgencyAndId, List<Departure>>() - if (this.vmDepartures.isNotEmpty()) { - departures[timetable.getServiceForToday()] = this.vmDepartures.flatMap { it.value }.sortedBy { it.timeTill(now) } - refreshAdapter(departures) - } else { - refreshAdapter(Departure.createDepartures(stopSegment!!.stop)) - hasDepartures = true - } - } - - private fun refreshAdapter(departures: Map<AgencyAndId, List<Departure>>?) { - if (departures != null) - sectionsPagerAdapter?.departures = departures - sectionsPagerAdapter?.notifyDataSetChanged() - selectTodayPage() - lastUpdated = Calendar.getInstance().timeInMillis - } - - override fun onVmPrepared() { - // println("onVmPrepared: ticked? ${ticked()}; vmBacked? ${favourite!!.isBackedByVm}") - if ((favourite!!.isBackedByVm || ticked()) && (timetableType == "departure")) { - getFavouriteDepartures() - } + subscribeForDepartures() } private fun showFab() { if (sourceType == SOURCE_TYPE_FAV) return - val stopSymbol = timetable.getStopCode(stopSegment!!.stop) - val favourites = FavouriteStorage.getFavouriteStorage(context) - if (!favourites.has(stopSymbol)) { + if (!favourites.has(stopCode)) { fab.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_favourite_empty, this.theme)) } fab.setOnClickListener { + /* todo if (!favourites.has(stopSymbol)) { val items = HashSet<StopSegment>() items.add(stopSegment!!) @@ -141,6 +106,7 @@ } else { Snackbar.make(it, getString(R.string.stop_already_fav), Snackbar.LENGTH_LONG) .setAction("Action", null).show() } + */ } } @@ -157,41 +123,40 @@ } private fun prepareOnDownloadListener() { val filter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED) - filter.addAction(VmClient.ACTION_READY) + filter.addAction(VmService.ACTION_READY) filter.addCategory(Intent.CATEGORY_DEFAULT) registerReceiver(receiver, filter) receiver.addOnTimetableDownloadListener(context) - if (sourceType == SOURCE_TYPE_STOP) { - receiver.addOnVmListener(context) - val intent = Intent(this, VmClient::class.java) - intent.putExtra("stop", stopSegment) - intent.action = "request" - startService(intent) + } + + private fun subscribeForDepartures() { + subscriptionId = if (sourceType == SOURCE_TYPE_STOP) { + providerProxy.subscribeForDepartures(stopCode, this, this) } else - favourite!!.registerOnVm(receiver, context) + favourite!!.subscribeForDepartures(this, context) } - override fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID) { - // println("onVm") - if (vmDepartures == null && this.vmDepartures.isEmpty() && hasDepartures) { - // println("\tbut noVM") - if (ticked()) { - // println("\t\tbut ticked") - refreshAdapterFromStop() - } + override fun onDeparturesReady(departures: Set<Departure>, plateId: Plate.ID) { + this.departures[plateId] = HashSet() + (this.departures[plateId]as HashSet).addAll(departures) + if (timetableType == TIMETABLE_TYPE_FULL) return - } - if (timetableType == "departure" && stopSegment!!.contains(plateId)) { - // println("\tthere’s still vm") - if (vmDepartures != null) - this.vmDepartures[plateId] = vmDepartures - else - this.vmDepartures.remove(plateId) - refreshAdapterFromStop() - } + refreshAdapter() } - private fun ticked() = Calendar.getInstance().timeInMillis - lastUpdated >= VmClient.TICK_6_ZINA_TIM_WITH_MARGIN + private fun refreshAdapter() { + if (timetableType == TIMETABLE_TYPE_FULL) + sectionsPagerAdapter!!.departures = fullDepartures + else { + val departures = HashMap<Int, List<Departure>>() + val now = Calendar.getInstance() + val tab = now.getMode() + val seconds = now.secondsAfterMidnight() + departures[tab] = this.departures.flatMap { it.value }.sortedBy { it.timeTill(seconds) } + sectionsPagerAdapter!!.departures = departures + } + sectionsPagerAdapter!!.notifyDataSetChanged() + } override fun onTimetableDownload(result: String?) { val message: String = when (result) { @@ -204,9 +169,7 @@ try { Snackbar.make(findViewById(R.id.stop_layout), message, Snackbar.LENGTH_LONG).show() } catch (e: IllegalArgumentException) { } - timetable = Timetable.getTimetable(this, true) - if (sourceType == SOURCE_TYPE_STOP) - refreshAdapterFromStop() + providerProxy.refreshTimetable(this) } private fun selectTodayPage() { @@ -214,7 +177,8 @@ tabs.getTabAt(sectionsPagerAdapter!!.todayTab())!!.select() } override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_stop, menu) + if (providerProxy.mode == ProviderProxy.MODE_FULL) + menuInflater.inflate(R.menu.menu_stop, menu) return true } @@ -222,22 +186,21 @@ override fun onOptionsItemSelected(item: MenuItem): Boolean { val id = item.itemId if (id == R.id.action_change_type) { - if (timetableType == "departure") { - timetableType = "full" + if (timetableType == TIMETABLE_TYPE_DEPARTURE) { + timetableType = TIMETABLE_TYPE_FULL item.icon = (ResourcesCompat.getDrawable(resources, R.drawable.ic_timetable_departure, this.theme)) sectionsPagerAdapter?.relativeTime = false - if (sourceType == SOURCE_TYPE_STOP) - refreshAdapter(timetable.getStopDepartures(stopSegment!!.stop)) - else - refreshAdapter(favourite!!.fullTimetable()) + if (fullDepartures.isEmpty()) + if (sourceType == SOURCE_TYPE_STOP) + fullDepartures.putAll(providerProxy.getFullTimetable(stopCode)) + else + fullDepartures.putAll(favourite!!.fullTimetable()) + refreshAdapter() } else { - timetableType = "departure" + timetableType = TIMETABLE_TYPE_DEPARTURE item.icon = (ResourcesCompat.getDrawable(resources, R.drawable.ic_timetable_full, this.theme)) sectionsPagerAdapter?.relativeTime = true - if (sourceType == SOURCE_TYPE_STOP) - refreshAdapterFromStop() - else - refreshAdapter(favourite!!.allDepartures()) + refreshAdapter() } return true } @@ -248,18 +211,14 @@ override fun onDestroy() { super.onDestroy() receiver.removeOnTimetableDownloadListener(context) - if (sourceType == SOURCE_TYPE_STOP) { - receiver.removeOnVmListener(context) - val intent = Intent(this, VmClient::class.java) - intent.putExtra("stop", stopSegment) - intent.action = "remove" - startService(intent) - } else - favourite!!.deregisterOnVm(receiver, context) + if (sourceType == SOURCE_TYPE_STOP) + providerProxy.unsubscribeFromDepartures(subscriptionId, this) + else + favourite!!.unsubscribeFromDepartures(subscriptionId, this) unregisterReceiver(receiver) } - inner class SectionsPagerAdapter(fm: FragmentManager, var departures: Map<AgencyAndId, List<Departure>>?) : FragmentStatePagerAdapter(fm) { + inner class SectionsPagerAdapter(fm: FragmentManager, var departures: Map<Int, List<Departure>>?) : FragmentStatePagerAdapter(fm) { var relativeTime = true override fun getItem(position: Int): Fragment { @@ -267,28 +226,7 @@ if (departures == null) return PlaceholderFragment.newInstance(null, relativeTime) { updateFabVisibility(it) } if (departures!!.isEmpty()) return PlaceholderFragment.newInstance(ArrayList(), relativeTime) { updateFabVisibility(it) } - val sat = try { - timetable.getServiceFor(Calendar.SATURDAY) - } catch (e: IllegalArgumentException) { - null - } - val sun = try { - timetable.getServiceFor(Calendar.SUNDAY) - } catch (e: IllegalArgumentException) { - null - } - val list: List<Departure> = when (position) { - 1 -> departures!![sat] ?: ArrayList() - 2 -> departures!![sun] ?: ArrayList() - 0 -> try { - departures!! - .filter { it.key != sat && it.key != sun } - .toList()[0].second - } catch (e: IndexOutOfBoundsException) { - ArrayList<Departure>() - } - else -> throw IndexOutOfBoundsException("No tab at index $position") - } + val list: List<Departure> = departures!![position] ?: ArrayList() return PlaceholderFragment.newInstance(list, relativeTime) { updateFabVisibility(it) } } @@ -303,7 +241,7 @@ return Calendar.getInstance().getMode() } } - class PlaceholderFragment: Fragment() { + class PlaceholderFragment : Fragment() { lateinit var updater: (Int) -> Unit override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val rootView = inflater.inflate(R.layout.fragment_stop, container, false) diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt index 9562d8d1b686e77cf39bd9428497a6a05231801d..a81d3536efc67b4b34a1b4b77337be6ba2379288 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopSpecifyActivity.kt @@ -30,7 +30,7 @@ providerProxy.getSheds(name) { val layoutManager = LinearLayoutManager(this) val departuresList: RecyclerView = list_view - departuresList.adapter = ShedAdapter(this, it) + departuresList.adapter = ShedAdapter(this, it, name) departuresList.layoutManager = layoutManager } /*val timetable = Timetable.getTimetable(this) @@ -41,7 +41,7 @@ setSupportActionBar(toolbar) supportActionBar?.title = name } - class ShedAdapter(val context: Context, private val values: Map<String, Set<String>>) : + class ShedAdapter(val context: Context, private val values: Map<String, Set<String>>, private val stopName: String) : RecyclerView.Adapter<ShedAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val context = parent.context @@ -59,11 +59,12 @@ val code = values.keys.sorted()[position] val intent = Intent(context, StopActivity::class.java) intent.putExtra(StopActivity.SOURCE_TYPE, StopActivity.SOURCE_TYPE_STOP) intent.putExtra(StopActivity.EXTRA_STOP_CODE, code) + intent.putExtra(StopActivity.EXTRA_STOP_NAME, stopName) context.startActivity(intent) } holder.stopCode.text = values.keys.sorted()[position] holder.stopHeadlines.text = values.entries.sortedBy { it.key }[position].value - .sortedBy { it.split(" → ")[0].toInt() } // fixme<p:1> natural sort + .sortedBy { it.split(" → ")[0].toInt() } .joinToString() } diff --git a/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt b/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt index 2c8e92efd8f69559ae0ec5cc2eadda253d49c80f..0f6675399ffa2dc686306220cb12ecc95d9b0cc2 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt @@ -4,7 +4,6 @@ import android.content.* import com.google.gson.* import ml.adamsprogs.bimba.* import ml.adamsprogs.bimba.models.* -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId import java.util.Calendar @@ -34,11 +33,11 @@ val favouritesMap = Gson().fromJson(favouritesString, JsonObject::class.java) for ((name, jsonTimetables) in favouritesMap.entrySet()) { val timetables = HashSet<StopSegment>() jsonTimetables.asJsonArray.mapTo(timetables) { - val stopSegment = StopSegment(AgencyAndId(it.asJsonObject["stop"].asString), null) + val stopSegment = StopSegment(it.asJsonObject["stop"].asString, null) val plates = HashSet<Plate.ID>() it.asJsonObject["plates"].asJsonArray.mapTo(plates) { - Plate.ID(AgencyAndId(it.asJsonObject["line"].asString), - AgencyAndId(it.asJsonObject["stop"].asString), + Plate.ID(it.asJsonObject["line"].asString, + it.asJsonObject["stop"].asString, it.asJsonObject["headsign"].asString) } stopSegment.plates = plates @@ -90,12 +89,12 @@ for ((name, favourite) in favourites) { val timetables = JsonArray() for (timetable in favourite.segments) { val segment = JsonObject() - segment.addProperty("stop", timetable.stop.id) + segment.addProperty("stop", timetable.stop) val plates = JsonArray() for (plate in timetable.plates ?: HashSet()) { val element = JsonObject() - element.addProperty("stop", plate.stop.id) - element.addProperty("line", plate.line.id) + element.addProperty("stop", plate.stop) + element.addProperty("line", plate.line) element.addProperty("headsign", plate.headsign) plates.add(element) } @@ -115,9 +114,9 @@ fun merge(names: List, context: Context) { if (names.size < 2) return - val newCache = HashMap<AgencyAndId, ArrayList<Departure>>() + val newCache = HashMap<Int, ArrayList<Departure>>() names.forEach { - favourites[it]!!.fullDepartures.forEach { + favourites[it]!!.fullTimetable().forEach { if (newCache[it.key] == null) newCache[it.key] = ArrayList() newCache[it.key]!!.addAll(it.value) @@ -147,18 +146,6 @@ positionIndex.remove(oldName) favourites[newName] = favourite addIndex(newName) serialize() - } - - fun registerOnVm(receiver: MessageReceiver, context: Context) { - favourites.values.forEach { - it.registerOnVm(receiver, context) - } - } - - fun deregisterOnVm(receiver: MessageReceiver, context: Context) { - favourites.values.forEach { - it.deregisterOnVm(receiver, context) - } } operator fun get(name: String): Favourite? { diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/CacheManager.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/CacheManager.kt deleted file mode 100644 index 71130764f796ebe17e68897bd6ec9899accfe5b1..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/datasources/CacheManager.kt +++ /dev/null @@ -1,136 +0,0 @@ -package ml.adamsprogs.bimba.datasources - -import android.content.Context -import android.content.SharedPreferences -import ml.adamsprogs.bimba.models.Plate -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId - -class CacheManager private constructor(context: Context) { - companion object { - private var manager: CacheManager? = null - fun getCacheManager(context: Context): CacheManager { - return if (manager == null) { - manager = CacheManager(context) - manager!! - } else - manager!! - } - - val MAX_SIZE = 40 - } - - private var cachePreferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.cachePreferences.cache", Context.MODE_PRIVATE) - private var cacheHitsPreferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.cachePreferences.cacheHits", Context.MODE_PRIVATE) - - private var cache: HashMap<String, Plate> = HashMap() - private var cacheHits: HashMap<String, Int> = HashMap() - - fun keys(): List<Plate> { - return cache.map { - Plate(Plate.ID( - AgencyAndId.convertFromString(it.key.split("@")[0]), - AgencyAndId.convertFromString(it.key.split("@")[1].split(">")[0]), - it.key.split(">")[1]), - null) - } - } - - fun hasAll(plates: HashSet<Plate>): Boolean { - plates - .filterNot { has(it) } - .forEach { return false } - return true - } - - fun hasAny(plates: HashSet<Plate>): Boolean { - plates - .filter { has(it) } - .forEach { return true } - return false - } - - fun has(plate: Plate): Boolean { - return cache.containsKey(key(plate)) - } - - fun push(plates: HashSet<Plate>) { - val removeNumber = cache.size + plates.size - MAX_SIZE - val editor = cachePreferences.edit() - val editorCacheHits = cacheHitsPreferences.edit() - cacheHits.map { "${it.value}|${it.key}" }.sortedBy { it }.slice(0 until removeNumber).forEach { - val key = it.split("|")[1] - cache.remove(key) - editor.remove(key) - } - for (plate in plates) { - val key = key(plate) - cache[key] = plate - cacheHits[key] = 0 - editor.putString(key, cache[key].toString()) - editorCacheHits.putInt(key, 0) - } - editor.apply() - editorCacheHits.apply() - } - - fun push(plate: Plate) { - val editorCache = cachePreferences.edit() - val editorCacheHits = cacheHitsPreferences.edit() - if (cacheHits.size == MAX_SIZE) { - val key = cacheHits.minBy { it.value }?.key - cache.remove(key) - editorCache.remove(key) - cacheHits.remove(key) - editorCacheHits.remove(key) - } - val key = key(plate) - cache[key] = plate - cacheHits[key] = 0 - editorCache.putString(key, plate.toString()) - editorCacheHits.putInt(key, 0) - editorCache.apply() - editorCacheHits.apply() - } - - fun get(plates: HashSet<Plate>): HashSet<Plate> { - val result = HashSet<Plate>() - for (plate in plates) { - val value = get(plate) - if (value == null) - result.add(plate) - else - result.add(value) - } - return result - } - - fun get(plate: Plate): Plate? { - if (!has(plate)) - return null - val key = key(plate) - val hits = cacheHits[key] - if (hits != null) - cacheHits[key] = hits + 1 - return cache[key] - } - - fun recreate(stopDeparturesByPlates: Set<Plate>) { - stopDeparturesByPlates.forEach { cache[key(it)] = it } - } - - init { - cache = cacheFromString(cachePreferences.all) - @Suppress("UNCHECKED_CAST") - cacheHits = cacheHitsPreferences.all as HashMap<String, Int> - } - - private fun cacheFromString(preferences: Map<String, *>): HashMap<String, Plate> { - val result = HashMap<String, Plate>() - for ((key, value) in preferences.entries) { - result[key] = Plate.fromString(value as String) - } - return result - } - - private fun key(plate: Plate) = "${plate.id.line}@${plate.id.stop}>${plate.id.headsign}" -} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt index 7fcb3e1ac4c36f0e0bcafc7fcc995bb468e920c7..88cb8c58bb4adcab4ec092cfd9ecdd508ab369a3 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt @@ -94,7 +94,7 @@ prefsEditor.putString("etag", newETag) prefsEditor.apply() val oldDb = File(getSecondaryExternalFilesDir(), "timetable.db") - gtfsDb.renameTo(oldDb) // todo<p:1> delete old before downloading (may require stopping VmClient), and mutex with VmClient + gtfsDb.renameTo(oldDb) // todo<p:1> delete old before downloading (may require stopping VmService), and mutex with VmService cancelNotification() diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt index bf703cca766ddb831b2069b1e778f3a979858694..81820ca45a92a47eb8f3c126748eddcf2721644c 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmClient.kt @@ -1,256 +1,101 @@ package ml.adamsprogs.bimba.datasources -import android.app.Service -import android.content.Intent -import android.os.Handler -import android.os.HandlerThread -import android.os.IBinder -import android.os.Process.THREAD_PRIORITY_BACKGROUND -import com.google.gson.Gson -import ml.adamsprogs.bimba.NetworkStateReceiver -import ml.adamsprogs.bimba.calendarFromIso -import ml.adamsprogs.bimba.models.* -import okhttp3.FormBody -import okhttp3.OkHttpClient -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId -import ml.adamsprogs.bimba.secondsAfterMidnight +import com.google.gson.* +import kotlinx.coroutines.experimental.* +import ml.adamsprogs.bimba.models.suggestions.* +import okhttp3.* import java.io.IOException import java.util.* import kotlin.collections.HashMap import kotlin.collections.HashSet -import kotlin.concurrent.thread -class VmClient : Service() { +class VmClient { companion object { - const val ACTION_READY = "ml.adamsprogs.bimba.action.vm.ready" - const val EXTRA_DEPARTURES = "ml.adamsprogs.bimba.extra.vm.departures" - const val EXTRA_PLATE_ID = "ml.adamsprogs.bimba.extra.vm.plate" - const val TICK_6_ZINA_TIM = 12500L - const val TICK_6_ZINA_TIM_WITH_MARGIN = TICK_6_ZINA_TIM * 3 / 4 - } + private var vmClient: VmClient? = null - private var handler: Handler? = null - private val tick6ZinaTim: Runnable = object : Runnable { - override fun run() { - handler!!.postDelayed(this, TICK_6_ZINA_TIM) - try { - for (plateId in requests.keys) - downloadVM() - } catch (e: IllegalArgumentException) { - } + fun getVmStopClient(): VmClient { + if (vmClient == null) + vmClient = VmClient() + return vmClient!! } } - private val requests = HashMap<AgencyAndId, Set<Request>>() - private val vms = HashMap<AgencyAndId, Set<Plate>>() - private val timetable = try { - Timetable.getTimetable(this) - } catch (e: NullPointerException) { - null - } - - override fun onCreate() { - val thread = HandlerThread("ServiceStartArguments", THREAD_PRIORITY_BACKGROUND) - thread.start() - handler = Handler(thread.looper) - handler!!.postDelayed(tick6ZinaTim, TICK_6_ZINA_TIM) - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (timetable == null) - return START_NOT_STICKY - val stopSegment = intent?.getParcelableExtra<StopSegment>("stop")!! - if (stopSegment.plates == null) - throw EmptyStopSegmentException() - val action = intent.action - val once = intent.getBooleanExtra("once", false) - if (action == "request") { - if (isAlreadyRequested(stopSegment)) { - incrementRequest(stopSegment) - sendResult(stopSegment) - } else { - if (!once) - addRequest(stopSegment) - thread { - downloadVM(stopSegment) - } - } - } else if (action == "remove") { - decrementRequest(stopSegment) - cleanRequests() + suspend fun getSheds(name: String): Map<String, Set<String>> { + val response = makeRequest("getBollardsByStopPoint", """{"name": "$name"}""") + if (!response.has("success")) + return emptyMap() + val rootObject = response["success"].asJsonObject["bollards"].asJsonArray + val result = HashMap<String, Set<String>>() + rootObject.forEach { + val code = it.asJsonObject["bollard"].asJsonObject["tag"].asString + result[code] = it.asJsonObject["directions"].asJsonArray.map { + """${it.asJsonObject["lineName"].asString} → ${it.asJsonObject["direction"].asString}""" + }.toSet() } - return START_STICKY + return result } - private fun cleanRequests() { - val newMap = HashMap<AgencyAndId, Set<Request>>() - requests.forEach { - newMap[it.key] = it.value.minus(it.value.filter { it.times == 0 }) - } - newMap.forEach { requests[it.key] = it.value } - } + /* + suspend fun getPlatesByStopPoint(code: String): Set<Plate.ID>? { + val getTimesResponse = makeRequest("getTimes", """{"symbol": "$code"}""") + val name = getTimesResponse["success"].asJsonObject["bollard"].asJsonObject["name"].asString - private fun addRequest(stopSegment: StopSegment) { - if (requests[stopSegment.stop] == null) { - requests[stopSegment.stop] = stopSegment.plates!! - .map { Request(it, 1) } - .toSet() - } else { - var req = requests[stopSegment.stop]!! - stopSegment.plates!!.forEach { - val plate = it - if (req.any { it.plate == plate }) { - req.filter { it.plate == plate }[0].times++ - } else { - req = req.plus(Request(it, 1)) - } - requests[stopSegment.stop] = req + val bollards = getBollardsByStopPoint(name) + return bollards.filter { + it.key == code + }.values.flatMap { + it.map { + val (line, headsign) = it.split(" → ") + Plate.ID(AgencyAndId(line), AgencyAndId(code), headsign) } + }.toSet() + }*/ + + suspend fun getStops(pattern: String): List<StopSuggestion> { + val response = withContext(DefaultDispatcher) { + makeRequest("getStopPoints", """{"pattern": "$pattern"}""") } - } - private fun sendResult(stop: StopSegment) { - vms[stop.stop]?.filter { - val plate = it - stop.plates!!.any { it == plate.id } - }?.forEach { sendResult(it.id, it.departures?.get(today())) } - } + if (!response.has("success")) + return emptyList() - private fun today(): AgencyAndId { - return timetable!!.getServiceForToday() - } + val points = response["success"].asJsonArray.map { it.asJsonObject } - private fun incrementRequest(stopSegment: StopSegment) { - stopSegment.plates!!.forEach { - val plateId = it - requests[it.stop]!!.filter { it.plate == plateId }.forEach { it.times++ } - } - } + val names = HashSet<String>() - private fun decrementRequest(stopSegment: StopSegment) { - stopSegment.plates!!.forEach { - val plateId = it - requests[it.stop]!!.filter { it.plate == plateId }.forEach { it.times-- } + points.forEach { + val name = it["name"].asString + names.add(name) } - } - private fun isAlreadyRequested(stopSegment: StopSegment): Boolean { - val platesIn = requests[stopSegment.stop]?.map { it.plate }?.toSet() - val platesOut = stopSegment.plates - if (platesIn == null || platesIn.isEmpty()) - return false - return (platesOut == platesIn || platesIn.containsAll(platesOut!!)) + return names.map { StopSuggestion(it, "", "") } } - - override fun onBind(intent: Intent): IBinder? { - return null - } - - override fun onDestroy() { - } - - @Synchronized - private fun downloadVM() { - vms.forEach { - downloadVM(StopSegment(it.key, it.value.map { it.id }.toSet())) - } - } - - private fun downloadVM(stopSegment: StopSegment) { - if (!NetworkStateReceiver.isNetworkAvailable(this)) { - vms[stopSegment.stop] = HashSet(stopSegment.plates!!.map { Plate(it, null) }.toSet()) - stopSegment.plates!!.forEach { - sendResult(it, null) - } - return - } - - val stopSymbol = timetable!!.getStopCode(stopSegment.stop) + suspend fun makeRequest(method: String, data: String): JsonObject { val client = OkHttpClient() val url = "http://www.peka.poznan.pl/vm/method.vm?ts=${Calendar.getInstance().timeInMillis}" - val formBody = FormBody.Builder() - .add("method", "getTimes") - .add("p0", "{\"symbol\": \"$stopSymbol\"}") - .build() + val body = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded; charset=UTF-8"), + "method=$method&p0=$data") val request = okhttp3.Request.Builder() .url(url) - .post(formBody) + .post(body) .build() + println("makeRequest: $request") + val responseBody: String? try { - responseBody = client.newCall(request).execute().body()?.string() - } catch (e: IOException) { - stopSegment.plates!!.forEach { - sendResult(it, null) - } - return - } - - if (responseBody?.get(0) == '<') { - stopSegment.plates!!.forEach { - sendResult(it, null) + responseBody = withContext(CommonPool) { + client.newCall(request).execute().body()?.string() } - return + } catch (e: IOException) { + return JsonObject() } - val javaRootMapObject = Gson().fromJson(responseBody, HashMap::class.java) - val times = (javaRootMapObject["success"] as Map<*, *>)["times"] as List<*> - stopSegment.plates!!.forEach { downloadVM(it, times) } - - } - - private fun downloadVM(plateId: Plate.ID, times: List<*>) { - val date = Calendar.getInstance() - val todayDay = "${date.get(Calendar.DATE)}".padStart(2, '0') - val todayMode = timetable!!.calendarToMode(AgencyAndId(timetable.getServiceForToday().id)) // fixme when no timetable use service == -1 for `today` - - val departures = HashSet<Departure>() - - times.forEach { - val thisLine = AgencyAndId((it as Map<*, *>)["line"] as String) - val thisHeadsign = it["direction"] as String - val thisPlateId = Plate.ID(thisLine, plateId.stop, thisHeadsign) - if (plateId == thisPlateId) { - val departureDay = (it["departure"] as String).split("T")[0].split("-")[2] - val departureTime = calendarFromIso(it["departure"] as String).secondsAfterMidnight() - val departure = Departure(plateId.line, todayMode, departureTime, false, - ArrayList(), it["direction"] as String, it["realTime"] as Boolean, - departureDay != todayDay, it["onStopPoint"] as Boolean) - departures.add(departure) - } + return try { + Gson().fromJson(responseBody, JsonObject::class.java) + } catch (e: JsonSyntaxException) { + JsonObject() } - - val departuresForPlate = HashMap<AgencyAndId, HashSet<Departure>>() - departuresForPlate[timetable.getServiceForToday()] = departures - val vm = vms[plateId.stop] ?: HashSet() - try { - (vm as HashSet).remove(vm.filter { it.id == plateId }[0]) - } catch (e: IndexOutOfBoundsException) { - } - (vm as HashSet).add(Plate(plateId, departuresForPlate)) - vms[plateId.stop] = vm - if (departures.isEmpty()) - sendResult(plateId, null) - else - sendResult(plateId, departures) } - - private fun sendResult(plateId: Plate.ID, departures: HashSet<Departure>?) { - val broadcastIntent = Intent() - broadcastIntent.action = ACTION_READY - broadcastIntent.addCategory(Intent.CATEGORY_DEFAULT) - if (departures != null) - broadcastIntent.putStringArrayListExtra(EXTRA_DEPARTURES, departures.map { it.toString() } as ArrayList) - broadcastIntent.putExtra(EXTRA_PLATE_ID, plateId) - sendBroadcast(broadcastIntent) - } - - data class Request(val plate: Plate.ID, var times: Int) - - class EmptyStopSegmentException : Exception() } - -//note application stops the service on exit - diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmService.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmService.kt new file mode 100644 index 0000000000000000000000000000000000000000..d9829c86ccecc42d4ab362ade5e6e9a36be5511b --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmService.kt @@ -0,0 +1,187 @@ +package ml.adamsprogs.bimba.datasources + +import android.app.Service +import android.content.Intent +import android.os.* +import android.os.Process.THREAD_PRIORITY_BACKGROUND +import com.google.gson.JsonObject +import kotlinx.coroutines.experimental.android.UI +import kotlinx.coroutines.experimental.* +import ml.adamsprogs.bimba.NetworkStateReceiver +import ml.adamsprogs.bimba.calendarFromIso +import ml.adamsprogs.bimba.models.* +import ml.adamsprogs.bimba.secondsAfterMidnight +import java.util.* +import kotlin.collections.* + +class VmService : Service() { + companion object { + const val ACTION_READY = "ml.adamsprogs.bimba.action.vm.ready" + const val EXTRA_DEPARTURES = "ml.adamsprogs.bimba.extra.vm.departures" + const val EXTRA_PLATE_ID = "ml.adamsprogs.bimba.extra.vm.plate" + const val EXTRA_STOP_CODE = "ml.adamsprogs.bimba.extra.vm.stop" + const val TICK_6_ZINA_TIM = 12500L + const val TICK_6_ZINA_TIM_WITH_MARGIN = TICK_6_ZINA_TIM * 3 / 4 + } + + private var handler: Handler? = null + private val tick6ZinaTim: Runnable = object : Runnable { + override fun run() { + handler!!.postDelayed(this, TICK_6_ZINA_TIM) + try { + for (plateId in requests.keys) + launch(UI) { + withContext(DefaultDispatcher) { + downloadVM() + } + } + } catch (e: IllegalArgumentException) { + } + } + } + private val requests = HashMap<String, Int>() + private val vms = HashMap<String, Set<Plate>>() + + override fun onCreate() { + val thread = HandlerThread("ServiceStartArguments", THREAD_PRIORITY_BACKGROUND) + thread.start() + handler = Handler(thread.looper) + handler!!.postDelayed(tick6ZinaTim, TICK_6_ZINA_TIM) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val stopCode = intent?.getStringExtra("stop")!! + val action = intent.action + val once = intent.getBooleanExtra("once", false) + if (action == "request") { + if (isAlreadyRequested(stopCode)) { + incrementRequest(stopCode) + sendResult(stopCode) + } else { + if (!once) + addRequest(stopCode) + launch(UI) { + withContext(DefaultDispatcher) { + downloadVM(stopCode) + } + } + } + } else if (action == "remove") { + decrementRequest(stopCode) + cleanRequests() + } + return START_STICKY + } + + private fun cleanRequests() { + requests.forEach { + if (it.value <= 0) + requests.remove(it.key) + } + } + + private fun addRequest(stopCode: String) { + if (requests[stopCode] == null) + requests[stopCode] = 0 + requests[stopCode] = requests[stopCode]!! + 1 + } + + private fun incrementRequest(stopCode: String) { + requests[stopCode] = requests[stopCode]!! + 1 + } + + private fun decrementRequest(stopCode: String) { + requests[stopCode] = requests[stopCode]!! - 1 + } + + private fun isAlreadyRequested(stopCode: String): Boolean { + return stopCode in requests + } + + + override fun onBind(intent: Intent): IBinder? { + return null + } + + override fun onDestroy() { + } + + private suspend fun downloadVM() { + vms.forEach { + downloadVM(it.key) + } + } + + private suspend fun downloadVM(stopCode: String) { + if (!NetworkStateReceiver.isNetworkAvailable(this)) { + vms[stopCode] = emptySet() + sendResult(stopCode, null, null) + return + } + + val javaRootMapObject = VmClient.getVmStopClient().makeRequest("getTimes", """{"symbol": "$stopCode"}""") + + if (!javaRootMapObject.has("success")) { + sendResult(stopCode, null, null) + return + } + + val times = (javaRootMapObject["success"].asJsonObject)["times"].asJsonArray.map { it.asJsonObject } + parseTimes(stopCode, times) + } + + private fun parseTimes(stopCode: String, times: List<JsonObject>) { + val date = Calendar.getInstance() + val todayDay = "${date.get(Calendar.DATE)}".padStart(2, '0') + + val departures = HashMap<Plate.ID, HashSet<Departure>>() + + times.forEach { + val thisLine = it["line"].asString + val thisHeadsign = it["direction"].asString + val thisPlateId = Plate.ID(thisLine, stopCode, thisHeadsign) + if (departures[thisPlateId] == null) + departures[thisPlateId] = HashSet() + val departureDay = (it["departure"].asString).split("T")[0].split("-")[2] + val departureTime = calendarFromIso(it["departure"].asString).secondsAfterMidnight() + val departure = Departure(thisLine, listOf(-1), departureTime, false, + ArrayList(), it["direction"].asString, it["realTime"].asBoolean, + departureDay != todayDay, it["onStopPoint"].asBoolean) + departures[thisPlateId]!!.add(departure) + } + + departures.forEach { + val departuresForPlate = HashMap<Int, HashSet<Departure>>() + departuresForPlate[-1] = it.value + val vm = HashSet<Plate>() + vm.add(Plate(it.key, departuresForPlate)) + vms[stopCode] = vm + if (departures.isEmpty()) + sendResult(stopCode, it.key, null) + else + sendResult(stopCode, it.key, it.value) + } + + } + + private fun sendResult(stopCode: String) { + vms[stopCode]?.forEach { + sendResult(it.id.stop, it.id, it.departures?.get(-1)) + } + + } + + private fun sendResult(stopCode: String, plateId: Plate.ID?, departures: HashSet<Departure>?) { + val broadcastIntent = Intent() + broadcastIntent.action = ACTION_READY + broadcastIntent.addCategory(Intent.CATEGORY_DEFAULT) + if (departures != null) + broadcastIntent.putStringArrayListExtra(EXTRA_DEPARTURES, departures.map { it.toString() } as ArrayList) + broadcastIntent.putExtra(EXTRA_PLATE_ID, plateId) + broadcastIntent.putExtra(EXTRA_STOP_CODE, stopCode) + sendBroadcast(broadcastIntent) + } +} + +//note application stops the service on exit + diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmStopsClient.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/VmStopsClient.kt deleted file mode 100644 index 7f9d5e8562a5c1f064dbc9c7fb6c4f9b422cdf53..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/datasources/VmStopsClient.kt +++ /dev/null @@ -1,100 +0,0 @@ -package ml.adamsprogs.bimba.datasources - -import com.google.gson.* -import kotlinx.coroutines.experimental.* -import ml.adamsprogs.bimba.models.Plate -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId -import ml.adamsprogs.bimba.models.suggestions.* -import okhttp3.* -import java.io.IOException -import java.util.* -import kotlin.collections.HashMap -import kotlin.collections.HashSet - -class VmStopsClient { - companion object { - private var vmStopsClient: VmStopsClient? = null - - fun getVmStopClient(): VmStopsClient { - if (vmStopsClient == null) - vmStopsClient = VmStopsClient() - return vmStopsClient!! - } - } - - suspend fun getSheds(name: String): Map<String, Set<String>> { - val response = makeRequest("getBollardsByStopPoint", """{"name": "$name"}""") - if (!response.has("success")) - return emptyMap() - val rootObject = response["success"].asJsonObject["bollards"].asJsonArray - val result = HashMap<String, Set<String>>() - rootObject.forEach { - val code = it.asJsonObject["bollard"].asJsonObject["tag"].asString - result[code] = it.asJsonObject["directions"].asJsonArray.map { - """${it.asJsonObject["lineName"].asString} → ${it.asJsonObject["direction"].asString}""" - }.toSet() - } - return result - } - - /* - suspend fun getPlatesByStopPoint(code: String): Set<Plate.ID>? { - val getTimesResponse = makeRequest("getTimes", """{"symbol": "$code"}""") - val name = getTimesResponse["success"].asJsonObject["bollard"].asJsonObject["name"].asString - - val bollards = getBollardsByStopPoint(name) - return bollards.filter { - it.key == code - }.values.flatMap { - it.map { - val (line, headsign) = it.split(" → ") - Plate.ID(AgencyAndId(line), AgencyAndId(code), headsign) - } - }.toSet() - }*/ - - suspend fun getStops(pattern: String): List<StopSuggestion> { - val response = withContext(DefaultDispatcher) { - makeRequest("getStopPoints", """{"pattern": "$pattern"}""") - } - - if (!response.has("success")) - return emptyList() - - val points = response["success"].asJsonArray.map { it.asJsonObject } - - val names = HashSet<String>() - - points.forEach { - val name = it["name"].asString - names.add(name) - } - - return names.map { StopSuggestion(it, "", "") } - } - - private suspend fun makeRequest(method: String, data: String): JsonObject { - val client = OkHttpClient() - val url = "http://www.peka.poznan.pl/vm/method.vm?ts=${Calendar.getInstance().timeInMillis}" - val body = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded; charset=UTF-8"), - "method=$method&p0=$data") - val request = okhttp3.Request.Builder() - .url(url) - .post(body) - .build() - println("makeRequest: $request") - - - val responseBody: String? - try { - responseBody = withContext(CommonPool) { - client.newCall(request).execute().body()?.string() - } - } catch (e: IOException) { - return JsonObject() - } - - - return Gson().fromJson(responseBody, JsonObject::class.java) - } -} diff --git a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt index cc04631c1708de25c1758299bc19ba0978599b2c..e398189fe5652b37dd473d40edf9927cf620e31a 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt @@ -80,7 +80,9 @@ else -> StopActivity.MODE_WORKDAYS } } -internal fun CharSequence.safeSplit(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List<String> { +internal fun CharSequence.safeSplit(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List<String>? { + if (this == "null") + return null if (this == "") return ArrayList() return this.split(*delimiters, ignoreCase = ignoreCase, limit = limit) diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt index 9989a5a13126f8a19094985c47c289382071c70b..35aeba4e97ff3df8a71f5742d7b49f2fb8a2bb8e 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Departure.kt @@ -1,6 +1,5 @@ package ml.adamsprogs.bimba.models -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId import ml.adamsprogs.bimba.safeSplit import ml.adamsprogs.bimba.secondsAfterMidnight import java.io.Serializable @@ -8,7 +7,7 @@ import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap -data class Departure(val line: AgencyAndId, val mode: List<Int>, val time: Int, val lowFloor: Boolean, //time in seconds since midnight +data class Departure(val line: String, val mode: List<Int>, val time: Int, val lowFloor: Boolean, //time in seconds since midnight val modification: List<String>, val headsign: String, val vm: Boolean = false, var tomorrow: Boolean = false, val onStop: Boolean = false) { @@ -28,7 +27,7 @@ companion object { private fun filterDepartures(departures: List<Departure>, relativeTo: Int = Calendar.getInstance().secondsAfterMidnight()): Array<Serializable> { val filtered = ArrayList<Departure>() - val lines = HashMap<AgencyAndId, Int>() + val lines = HashMap<String, Int>() val sortedDepartures = departures.sortedBy { it.timeTill(relativeTo) } for (departure in sortedDepartures) { val timeTill = departure.timeTill(relativeTo) @@ -42,15 +41,15 @@ } return arrayOf(filtered, lines.all { it.value >= 3 }) } - fun createDepartures(stopId: AgencyAndId): Map<AgencyAndId, List<Departure>> { + /*fun createDepartures(stopCode: String): Map<String, List<Departure>> { val timetable = Timetable.getTimetable() - val departures = timetable.getStopDepartures(stopId) + val departures = timetable.getStopDepartures(stopCode) return rollDepartures(departures) - } + }*/ - fun rollDepartures(departures: Map<AgencyAndId, List<Departure>>): Map<AgencyAndId, List<Departure>> { //todo<p:2> it'd be nice to roll from tomorrow's real mode (Fri->Sat, Sat->Sun, Sun->Mon) - val rolledDepartures = HashMap<AgencyAndId, List<Departure>>() + fun rollDepartures(departures: Map<Int, List<Departure>>): Map<Int, List<Departure>> { //todo<p:2> it'd be nice to roll from tomorrow's real mode (Fri->Sat, Sat->Sun, Sun->Mon) + val rolledDepartures = HashMap<Int, List<Departure>>() departures.keys.forEach { val (filtered, isFull) = filterDepartures(departures[it]!!) if (isFull as Boolean) { @@ -80,9 +79,9 @@ fun fromString(string: String): Departure { val array = string.split("|") if (array.size != 9) throw IllegalArgumentException() - val modification = array[4].safeSplit(";") - return Departure(AgencyAndId.convertFromString(array[0]), - array[1].safeSplit(";").map { Integer.parseInt(it) }, + val modification = array[4].safeSplit(";")!! + return Departure(array[0], + array[1].safeSplit(";")!!.map { Integer.parseInt(it) }, Integer.parseInt(array[2]), array[3] == "true", modification, array[5], array[6] == "true", array[7] == "true", array[8] == "true") @@ -96,5 +95,5 @@ time += 24 * 60 * 60 return (time - relativeTo) / 60 } - val lineText: String = line.id + val lineText: String = line } \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt index 469a7cdc28422e9fbc937c2e77c7fc4a59f2242b..87df063adc773a421f92d0c37b6d34c110c8af90 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Favourite.kt @@ -3,42 +3,27 @@ import android.content.* import android.os.* import ml.adamsprogs.bimba.* -import ml.adamsprogs.bimba.datasources.VmClient -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId import java.io.File import java.math.BigInteger import java.security.SecureRandom -import java.util.Calendar import kotlin.collections.* -class Favourite : Parcelable, MessageReceiver.OnVmListener { - private var isRegisteredOnVmListener: Boolean = false +class Favourite : Parcelable, ProviderProxy.OnDeparturesReadyListener { private val cacheDir: File + private lateinit var listener: ProviderProxy.OnDeparturesReadyListener var name: String private set var segments: HashSet<StopSegment> private set - private var vmDepartures = HashMap<Plate.ID, List<Departure>>() - var fullDepartures: Map<AgencyAndId, List<Departure>> = HashMap() - private set - val timetable = Timetable.getTimetable() + private var fullDepartures: Map<Int, List<Departure>> = HashMap() + private var cache: Set<Departure> = HashSet() val size get() = segments.sumBy { it.size } - val isBackedByVm - get() = vmDepartures.isNotEmpty() - private val onVmPreparedListeners = HashSet<OnVmPreparedListener>() - - fun addOnVmPreparedListener(listener: OnVmPreparedListener) { - onVmPreparedListeners.add(listener) - } - - fun removeOnVmPreparedListener(listener: OnVmPreparedListener) { - onVmPreparedListeners.remove(listener) - } + private val providerProxy: ProviderProxy constructor(parcel: Parcel) { this.name = parcel.readString() @@ -54,26 +39,29 @@ val mapDir = File(parcel.readString()) val mapString = mapDir.readText() - val map = HashMap<AgencyAndId, List<Departure>>() - mapString.safeSplit("%").forEach { + val map = HashMap<Int, List<Departure>>() + mapString.safeSplit("%")!!.forEach { it -> val (k, v) = it.split("#") - map[AgencyAndId(k)] = v.split("&").map { Departure.fromString(it) } + map[k.toInt()] = v.split("&").map { Departure.fromString(it) } } this.fullDepartures = map mapDir.delete() + providerProxy = ProviderProxy() } - constructor(name: String, segments: HashSet<StopSegment>, cache: Map<AgencyAndId, List<Departure>>, context: Context) { + constructor(name: String, segments: HashSet<StopSegment>, cache: Map<Int, List<Departure>>, context: Context) { this.fullDepartures = cache this.name = name this.segments = segments this.cacheDir = context.cacheDir + providerProxy = ProviderProxy(context) } constructor(name: String, timetables: HashSet<StopSegment>, context: Context) { this.name = name this.segments = timetables this.cacheDir = context.cacheDir + providerProxy = ProviderProxy(context) } @@ -94,7 +82,7 @@ dest?.writeString(mapFile.absolutePath) var isFirst = true var map = "" - fullDepartures.forEach { + fullDepartures.forEach { it -> if (isFirst) isFirst = false else @@ -105,15 +93,6 @@ } mapFile.writeText(map) } - private fun filterVmDepartures() { - val now = Calendar.getInstance().secondsAfterMidnight() - this.vmDepartures.forEach { - val newVms = it.value - .filter { it.timeTill(now) >= 0 }.sortedBy { it.timeTill(now) } - this.vmDepartures[it.key] = newVms - } - } - fun delete(plateId: Plate.ID) { segments.forEach { it.remove(plateId) @@ -121,35 +100,6 @@ } removeFromCache(plateId) } - fun registerOnVm(receiver: MessageReceiver, context: Context) { - if (!isRegisteredOnVmListener) { - receiver.addOnVmListener(this) - isRegisteredOnVmListener = true - - - segments.forEach { - val intent = Intent(context, VmClient::class.java) - intent.putExtra("stop", it) - intent.action = "request" - context.startService(intent) - } - } - } - - fun deregisterOnVm(receiver: MessageReceiver, context: Context) { - if (isRegisteredOnVmListener) { - receiver.removeOnVmListener(this) - isRegisteredOnVmListener = false - - segments.forEach { - val intent = Intent(context, VmClient::class.java) - intent.putExtra("stop", it) - intent.action = "remove" - context.startService(intent) - } - } - } - fun rename(newName: String) { name = newName } @@ -164,80 +114,39 @@ return arrayOfNulls(size) } } - fun nextDeparture(): Departure? { - val now = Calendar.getInstance().secondsAfterMidnight() - filterVmDepartures() - if (segments.isEmpty() && vmDepartures.isEmpty()) - return null + fun nextDeparture() = + if (cache.isEmpty()) + null + else + cache.sortedBy { it.time }[0] - if (vmDepartures.isNotEmpty()) { - return vmDepartures.flatMap { it.value } - .minBy { - it.timeTill(now) - } - } - val full = fullTimetable() - - val twoDayDepartures = try { - Departure.rollDepartures(full)[timetable.getServiceForToday()] - } catch (e: IllegalArgumentException) { - listOf<Departure>() - } - - if (twoDayDepartures?.isEmpty() != false) - return null - - return twoDayDepartures[0] + fun fullTimetable(): Map<Int, List<Departure>> { + if (fullDepartures.isEmpty()) + fullDepartures = providerProxy.getFullTimetable(segments) + return fullDepartures } - fun allDepartures(): Map<AgencyAndId, List<Departure>> { - if (vmDepartures.isNotEmpty()) { - val now = Calendar.getInstance().secondsAfterMidnight() - val departures = HashMap<AgencyAndId, List<Departure>>() - val today = timetable.getServiceForToday() - departures[today] = vmDepartures.flatMap { it.value }.sortedBy { it.timeTill(now) } - return departures + private fun removeFromCache(plate: Plate.ID) { + val map = HashMap<Int, List<Departure>>() + fullDepartures + fullDepartures.forEach { it -> + map[it.key] = it.value.filter { plate.line != it.line || plate.headsign != it.headsign } } - - val departures = fullTimetable() - return Departure.rollDepartures(departures) + fullDepartures = map } - fun fullTimetable() = - if (fullDepartures.isNotEmpty()) - fullDepartures - else { - fullDepartures = timetable.getStopDeparturesBySegments(segments) - fullDepartures - - } - - - override fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID) { - val now = Calendar.getInstance().secondsAfterMidnight() - if (segments.any { it.contains(plateId) }) { - if (vmDepartures == null) - this.vmDepartures.remove(plateId) - else - this.vmDepartures[plateId] = vmDepartures.sortedBy { it.timeTill(now) } - } - filterVmDepartures() - onVmPreparedListeners.forEach { - it.onVmPrepared() - } + fun subscribeForDepartures(listener: ProviderProxy.OnDeparturesReadyListener, context: Context): String { + this.listener = listener + return providerProxy.subscribeForDepartures(segments, this, context) } - private fun removeFromCache(plate: Plate.ID) { - val map = HashMap<AgencyAndId, List<Departure>>() - fullDepartures - fullDepartures.forEach { - map[it.key] = it.value.filter { plate.line != it.line || plate.headsign != it.headsign } - } - fullDepartures = map + override fun onDeparturesReady(departures: Set<Departure>, plateId: Plate.ID) { + cache = departures + listener.onDeparturesReady(departures, plateId) } - interface OnVmPreparedListener { - fun onVmPrepared() + fun unsubscribeFromDepartures(uuid: String, context: Context) { + providerProxy.unsubscribeFromDepartures(uuid, context) } } diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt index 43ff657d166e562c108df2eeacc2aa2031be0cd1..8fb050a63053bf54097da8d1048f746fe74bae98 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Plate.kt @@ -1,9 +1,8 @@ package ml.adamsprogs.bimba.models -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId import java.io.Serializable -data class Plate(val id: ID, val departures: HashMap<AgencyAndId, HashSet<Departure>>?) { +data class Plate(val id: ID, val departures: HashMap<Int, HashSet<Departure>>?) { override fun toString(): String { var result = "${id.line}=${id.stop}=${id.headsign}={" if (departures != null) { @@ -19,29 +18,26 @@ return result } companion object { - fun fromString(string: String): Plate { + /*fun fromString(string: String): Plate { val (lineStr, stopStr, headsign, departuresString) = string.split("=") - val line = AgencyAndId.convertFromString(lineStr) - val stop = AgencyAndId.convertFromString(stopStr) - val departures = HashMap<AgencyAndId, HashSet<Departure>>() + val departures = HashMap<Int, HashSet<Departure>>() departuresString.replace("{", "").replace("}", "").split(";") .filter { it != "" } .forEach { try { val (serviceStr, depStr) = it.split(":") val dep = Departure.fromString(depStr) - val service = AgencyAndId.convertFromString(serviceStr) - if (departures[service] == null) - departures[service] = HashSet() - departures[service]!!.add(dep) + if (departures[serviceStr] == null) + departures[serviceStr] = HashSet() + departures[serviceStr]!!.add(dep) } catch (e: IllegalArgumentException) { } } - return Plate(ID(line, stop, headsign), departures) + return Plate(ID(lineStr, stopStr, headsign), departures) } - fun join(set: Set<Plate>): HashMap<AgencyAndId, ArrayList<Departure>> { - val departures = HashMap<AgencyAndId, ArrayList<Departure>>() + fun join(set: Set<Plate>): HashMap<String, ArrayList<Departure>> { + val departures = HashMap<String, ArrayList<Departure>>() for (plate in set) { for ((mode, d) in plate.departures!!) { if (departures[mode] == null) @@ -53,15 +49,15 @@ for ((mode, _) in departures) { departures[mode]?.sortBy { it.time } } return departures - } + }*/ } - data class ID(val line: AgencyAndId, val stop: AgencyAndId, val headsign: String) : Serializable { + data class ID(val line: String, val stop: String, val headsign: String) : Serializable { companion object { fun fromString(string: String): ID { val (line, stop, headsign) = string.split("|") - return ID(AgencyAndId.convertFromString(line), - AgencyAndId.convertFromString(stop), headsign) + return ID(line, + stop, headsign) } } diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt b/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt index 5197e6e17237a3b8ab3e28880b875321ddb6fda3..07148cbe06fa0bc5f3d6074db888601dc8f0d469 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/StopSegment.kt @@ -2,13 +2,12 @@ package ml.adamsprogs.bimba.models import android.os.Parcel import android.os.Parcelable -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId import ml.adamsprogs.bimba.safeSplit -data class StopSegment(val stop: AgencyAndId, var plates: Set<Plate.ID>?) : Parcelable { +data class StopSegment(val stop: String, var plates: Set<Plate.ID>?) : Parcelable { constructor(parcel: Parcel) : this( - parcel.readSerializable() as AgencyAndId, - parcel.readString().safeSplit(";").map { Plate.ID.fromString(it) }.toSet() + parcel.readSerializable() as String, + parcel.readString().safeSplit(";")?.map { Plate.ID.fromString(it) }?.toSet() ) companion object CREATOR : Parcelable.Creator<StopSegment> { @@ -19,10 +18,6 @@ override fun newArray(size: Int): Array<StopSegment?> { return arrayOfNulls(size) } - } - - fun fillPlates() { - plates = Timetable.getTimetable().getPlatesForStop(stop) } override fun writeToParcel(dest: Parcel?, flags: Int) { @@ -30,7 +25,7 @@ dest?.writeSerializable(stop) if (plates != null) dest?.writeString(plates!!.joinToString(";") { it.toString() }) else - dest?.writeString("") + dest?.writeString("null") } override fun describeContents(): Int { @@ -56,12 +51,14 @@ return false } override fun hashCode(): Int { - return super.hashCode() + var hashCode = stop.hashCode() + plates?.forEach { hashCode = 31 * hashCode + it.hashCode() } + return hashCode } - fun contains(plateId: Plate.ID): Boolean { + operator fun contains(plateId: Plate.ID): Boolean { if (plates == null) - return false + return plateId.stop == stop return plates!!.contains(plateId) } diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt index f09c969c9554054ce02e4ffd3b653422e795d39a..f00bd5680b147a5006f477d30922a8ba5b6df40a 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/Timetable.kt @@ -3,6 +3,7 @@ import android.annotation.SuppressLint import android.content.Context import android.database.* +import android.database.sqlite.SQLiteCantOpenDatabaseException import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteException import ml.adamsprogs.bimba.* @@ -139,9 +140,9 @@ AWF73 -> {10 → Franowo, 29 → Franowo, 6 → Miłostowo, 5 → Stomil, 18 → Franowo, 15 → Franowo, 12 → Starołęka, 74 → Os. Orła Białego} */ } - fun getStopName(stopId: AgencyAndId): String { - val cursor = db!!.rawQuery("select stop_name from stops where stop_id = ?", - arrayOf(stopId.id)) + fun getStopName(stopCode: String): String { + val cursor = db!!.rawQuery("select stop_name from stops where stop_code = ?", + arrayOf(stopCode)) cursor.moveToNext() val name = cursor.getString(0) cursor.close() @@ -149,9 +150,9 @@ return name } - fun getStopCode(stopId: AgencyAndId): String { + fun getStopCode(stopId: String): String { val cursor = db!!.rawQuery("select stop_code from stops where stop_id = ?", - arrayOf(stopId.id)) + arrayOf(stopId)) cursor.moveToNext() val code = cursor.getString(0) cursor.close() @@ -159,16 +160,16 @@ return code } - fun getStopDepartures(stopId: AgencyAndId): Map<AgencyAndId, List<Departure>> { - val map = HashMap<AgencyAndId, ArrayList<Departure>>() + fun getStopDepartures(stopCode: String): Map<String, List<Departure>> { + val map = HashMap<String, ArrayList<Departure>>() val cursor = db!!.rawQuery("select route_id, service_id, departure_time, " + "wheelchair_accessible, stop_sequence, trip_id, trip_headsign, route_desc " + - "from stop_times natural join trips natural join routes where stop_id = ?", - arrayOf(stopId.id)) + "from stop_times natural join trips natural join routes where stop_code = ?", + arrayOf(stopCode)) while (cursor.moveToNext()) { - val line = AgencyAndId(cursor.getString(0)) - val service = AgencyAndId(cursor.getInt(1).toString()) + val line = cursor.getString(0) + val service = cursor.getInt(1).toString() val mode = calendarToMode(service) val time = parseTime(cursor.getString(2)) val lowFloor = cursor.getInt(3) == 1 @@ -192,10 +193,10 @@ return map } - fun getStopDeparturesBySegments(segments: HashSet<StopSegment>): Map<AgencyAndId, List<Departure>> { + fun getStopDeparturesBySegments(segments: Set<StopSegment>): Map<String, List<Departure>> { val wheres = segments.flatMap { it.plates?.map { - "(stop_id = ${it.stop} and route_id = '${it.line}' and trip_headsign = '${it.headsign}')" + "(stop_code = ${it.stop} and route_id = '${it.line}' and trip_headsign = '${it.headsign}')" } ?: listOf() }.joinToString(" or ") @@ -208,12 +209,12 @@ cursor.close() return map } - private fun parseDeparturesCursor(cursor: Cursor): Map<AgencyAndId, List<Departure>> { - val map = HashMap<AgencyAndId, ArrayList<Departure>>() + private fun parseDeparturesCursor(cursor: Cursor): Map<String, List<Departure>> { + val map = HashMap<String, ArrayList<Departure>>() while (cursor.moveToNext()) { - val line = AgencyAndId(cursor.getString(0)) - val service = AgencyAndId(cursor.getInt(1).toString()) + val line = cursor.getString(0) + val service = cursor.getInt(1).toString() val mode = calendarToMode(service) val time = parseTime(cursor.getString(2)) val lowFloor = cursor.getInt(3) == 1 @@ -245,10 +246,10 @@ cal.set(JCalendar.SECOND, s.toInt()) return cal.secondsAfterMidnight() } - fun calendarToMode(serviceId: AgencyAndId): List<Int> { + private fun calendarToMode(serviceId: String): List<Int> { val days = ArrayList<Int>() val cursor = db!!.rawQuery("select * from calendar where service_id = ?", - arrayOf(serviceId.id)) + arrayOf(serviceId)) cursor.moveToNext() (1 until 7).forEach { @@ -264,9 +265,9 @@ val explanations = ArrayList () tripId.modification.forEach { if (it.stopRange != null) { if (stopSequence in it.stopRange) - explanations.add(routeModifications[it.id.id]!!) + explanations.add(routeModifications[it.id]!!) } else { - explanations.add(routeModifications[it.id.id]!!) + explanations.add(routeModifications[it.id]!!) } } @@ -297,15 +298,15 @@ if (modification != "") { modification.split(",").forEach { try { val (id, start, end) = it.split(":") - modifications.add(Trip.ID.Modification(AgencyAndId(id), IntRange(start.toInt(), end.toInt()))) + modifications.add(Trip.ID.Modification(id, IntRange(start.toInt(), end.toInt()))) } catch (e: Exception) { - modifications.add(Trip.ID.Modification(AgencyAndId(it), null)) + modifications.add(Trip.ID.Modification(it, null)) } } } - return Trip.ID(rawId, AgencyAndId(rawId.split("^")[0]), modifications, isMain) + return Trip.ID(rawId, rawId.split("^")[0], modifications, isMain) } else - return Trip.ID(rawId, AgencyAndId(rawId), HashSet(), false) + return Trip.ID(rawId, rawId, HashSet(), false) } @SuppressLint("Recycle") @@ -345,19 +346,19 @@ cursor.close() return validTill } - fun getServiceForToday(): AgencyAndId { + fun getServiceForToday(): String { val today = JCalendar.getInstance().get(JCalendar.DAY_OF_WEEK) return getServiceFor(today) } - fun getServiceForTomorrow(): AgencyAndId { + fun getServiceForTomorrow(): String { val tomorrow = JCalendar.getInstance() tomorrow.add(JCalendar.DAY_OF_MONTH, 1) val tomorrowDoW = tomorrow.get(JCalendar.DAY_OF_WEEK) return getServiceFor(tomorrowDoW) } - fun getServiceFor(day: Int): AgencyAndId { + fun getServiceFor(day: Int): String { val dayColumn = arrayOf("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")[((day + 5) % 7)] val cursor = db!!.rawQuery("select service_id from calendar where $dayColumn = 1", null) @@ -366,20 +367,20 @@ cursor.moveToNext() try { service = cursor.getInt(0) cursor.close() - return AgencyAndId(service.toString()) + return service.toString() } catch (e: CursorIndexOutOfBoundsException) { throw IllegalArgumentException() } } - fun getPlatesForStop(stop: AgencyAndId): Set<Plate.ID> { + fun getPlatesForStop(stop: String): Set<Plate.ID> { val plates = HashSet<Plate.ID>() val cursor = db!!.rawQuery("select route_id, trip_headsign " + - "from stop_times natural join trips where stop_id = ? " + - "group by route_id, trip_headsign", arrayOf(stop.id)) + "from stop_times natural join trips where stop_code = ? " + + "group by route_id, trip_headsign", arrayOf(stop)) while (cursor.moveToNext()) { - val routeId = AgencyAndId(cursor.getString(0)) + val routeId = cursor.getString(0) val headsign = cursor.getString(1) plates.add(Plate.ID(routeId, stop, headsign)) } @@ -388,13 +389,13 @@ cursor.close() return plates } - fun getTripGraphs(id: AgencyAndId): Array<TripGraph> { + fun getTripGraphs(id: String): Array<TripGraph> { val graphs = arrayOf(TripGraph(), TripGraph()) val cursor = db!!.rawQuery("select trip_id, trip_headsign, direction_id, stop_id, " + "stop_sequence, pickup_type, stop_name, zone_id " + "from stop_times natural join trips natural join stops" + - "where route_id = ?", arrayOf(id.id)) + "where route_id = ?", arrayOf(id)) while (cursor.moveToNext()) { val trip = cursor.getString(0) diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt index 6cef0867376e4bcf1b8aecf4df91fc3b683ede7f..76206a2b5e93f9683ff8dc07aa7011fa48c2a29e 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt @@ -9,10 +9,8 @@ import android.view.* import android.widget.* import ml.adamsprogs.bimba.R import android.view.LayoutInflater -import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.android.UI -import kotlinx.coroutines.experimental.async -import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.* import java.util.* import ml.adamsprogs.bimba.Declinator import ml.adamsprogs.bimba.collections.FavouriteStorage @@ -61,7 +59,7 @@ val favourite = favourites[position]!! holder.nameTextView.text = favourite.name holder.selectedOverlay.visibility = if (isSelected(position)) View.VISIBLE else View.INVISIBLE - holder.moreButton.setOnClickListener { + holder.moreButton.setOnClickListener { it -> val popup = PopupMenu(appContext, it) val inflater = popup.menuInflater popup.setOnMenuItemClickListener { @@ -75,9 +73,9 @@ inflater.inflate(R.menu.favourite_actions, popup.menu) popup.show() } - val nextDeparture = async(CommonPool) { + val nextDeparture = withContext(CommonPool) { favourite.nextDeparture() - }.await() + } val nextDepartureText: String val nextDepartureLineText: String diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/AgencyAndId.kt b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/AgencyAndId.kt deleted file mode 100644 index 66c8d620d3dfbcd85c3def1d14d5cfee300e878e..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/AgencyAndId.kt +++ /dev/null @@ -1,19 +0,0 @@ -package ml.adamsprogs.bimba.models.gtfs - -import java.io.Serializable - -data class AgencyAndId(val id: String) : Serializable, Comparable<AgencyAndId> { - override fun compareTo(other: AgencyAndId): Int { - return this.toString().compareTo(other.toString()) - } - - companion object { - fun convertFromString(str: String): AgencyAndId { - return AgencyAndId(str) - } - } - - override fun toString(): String { - return id - } -} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Route.kt b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Route.kt index ddba5daf28fd737a6c067c263d10c19ecc9d05d0..c3021d8a3c0991b4cee3150a5a2f9751a2ea8764 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Route.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Route.kt @@ -4,7 +4,7 @@ import android.os.Parcel import android.os.Parcelable -data class Route(val id: AgencyAndId, val agency: AgencyAndId, val shortName: String, +data class Route(val id: String, val agency: String, val shortName: String, val longName: String, val description: String, val type: Int, val colour: Int, val textColour: Int, val modifications: Map<String, String>) : Parcelable { companion object CREATOR : Parcelable.Creator<Route> { @@ -27,13 +27,13 @@ val fromSplit = from.split("^") val toSplit = to.split("^") val description = "${toSplit[0]}|${fromSplit[0]}" val modifications = createModifications(desc) - Route(AgencyAndId(id), AgencyAndId(agency), shortName, longName, description, + Route(id, agency, shortName, longName, description, type, colour, textColour, modifications) } else { val toSplit = desc.split("^") val description = toSplit[0] val modifications = createModifications(desc) - Route(AgencyAndId(id), AgencyAndId(agency), shortName, longName, description, + Route(id, agency, shortName, longName, description, type, colour, textColour, modifications) } } @@ -57,8 +57,8 @@ } @Suppress("UNCHECKED_CAST") constructor(parcel: Parcel) : this( - AgencyAndId(parcel.readString()), - AgencyAndId(parcel.readString()), + parcel.readString(), + parcel.readString(), parcel.readString(), parcel.readString(), parcel.readString(), @@ -68,8 +68,8 @@ parcel.readInt(), parcel.readSerializable() as HashMap<String, String>) override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(id.id) - parcel.writeString(agency.id) + parcel.writeString(id) + parcel.writeString(agency) parcel.writeString(shortName) parcel.writeString(longName) parcel.writeString(description) diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Trip.kt b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Trip.kt index 6e2de9c4c35d16fd71e0a31745ee277dc5947134..db797a31de4ba22531ebe14266ed73ed38e72254 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Trip.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/gtfs/Trip.kt @@ -1,9 +1,9 @@ package ml.adamsprogs.bimba.models.gtfs -data class Trip(val routeId: AgencyAndId, val serviceId: AgencyAndId, val id: ID, - val headsign: String, val direction: Int, val shapeId: AgencyAndId, +data class Trip(val routeId: String, val serviceId: String, val id: ID, + val headsign: String, val direction: Int, val shapeId: String, val wheelchairAccessible: Boolean) { - data class ID(val rawId:String, val id: AgencyAndId, val modification: Set<Modification>, val isMain: Boolean) { - data class Modification(val id: AgencyAndId, val stopRange: IntRange?) + data class ID(val rawId:String, val id: String, val modification: Set<Modification>, val isMain: Boolean) { + data class Modification(val id: String, val stopRange: IntRange?) } } \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/suggestions/StopSuggestion.kt b/app/src/main/java/ml/adamsprogs/bimba/models/suggestions/StopSuggestion.kt index af9fc5a346c5ef4d8dea9e54a4ef0e3a635cc836..cd3ef7762cb9f0e16a905cba2df8193e9d8b4568 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/models/suggestions/StopSuggestion.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/models/suggestions/StopSuggestion.kt @@ -3,7 +3,6 @@ import android.os.Parcel import android.os.Parcelable import ml.adamsprogs.bimba.R -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId class StopSuggestion(name: String, private val zone: String, private val zoneColour: String) : GtfsSuggestion(name){ @Suppress("UNCHECKED_CAST") diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml index 1c3647edb4ef835066979a1174d44dbd79359557..edbdd769a5229ecc162ac8bc3b128c86a91dbe4d 100644 --- a/app/src/main/res/xml/pref_main.xml +++ b/app/src/main/res/xml/pref_main.xml @@ -9,5 +9,10 @@ android:title="@string/title_timetable_source_url" /> <!-- todo intent get file (import) --> <!-- todo reset source --> + <SwitchPreference + android:defaultValue="false" + android:key="key_timetable_automatic_update" + android:summary="Automatically check for and download timetable updates" + android:title="Automatic updates" /> </PreferenceCategory> </PreferenceScreen> diff --git a/build.gradle b/build.gradle index abd71c41b0e3809a905d7e36ce37c4c9b60fb0b2..eb4b96ee435f6e9def2abe971a838aba0ac9b430 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.2.51' + ext.kotlin_version = '1.2.60' repositories { jcenter() maven { url 'https://maven.google.com' } @@ -9,7 +9,7 @@ //maven { url 'https://dl.bintray.com/guardian/android' } // TooLargeTool google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.3' + classpath 'com.android.tools.build:gradle:3.1.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong