Author: Adam <git@apiote.xyz>
ammend
app/src/main/java/ml/adamsprogs/bimba/api/Api.kt | 43 app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt | 51 + app/src/main/java/ml/adamsprogs/bimba/search/Results.kt | 142 +++ app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt | 127 ++ app/src/main/res/drawable/ic_circle_white_24dp.xml | 9 app/src/main/res/drawable/ic_square_white_24dp.xml | 5 app/src/main/res/drawable/ic_stop_black_24dp.xml | 5 app/src/main/res/layout/activity_results.xml | 43 app/src/main/res/layout/suggestion.xml | 46
diff --git a/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt b/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt new file mode 100644 index 0000000000000000000000000000000000000000..67be4d5e21984f6a8dbbf89f1f15195d84b20aad --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/api/Api.kt @@ -0,0 +1,43 @@ +package ml.adamsprogs.bimba.api + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLEncoder + +suspend fun queryItems(query: String): InputStream { + return request("https://bimba.apiote.xyz", "poznan_ztm", "items", mapOf("q" to query)) +} + +suspend fun locateItems(plusCode: String): InputStream { + return request("https://bimba.apiote.xyz", "poznan_ztm", "items", mapOf("near" to plusCode)) +} + +@Suppress("BlockingMethodInNonBlockingContext") +suspend fun request( + host: String, + feed: String, + resource: String, + params: Map<String, String> +): InputStream { + return withContext(Dispatchers.IO) { + val url = URL( + "$host/api/$feed/$resource${ + params.map { + "${it.key}=${ + URLEncoder.encode( + it.value, + "utf-8" + ) + }" + }.joinToString("&", "?") + }" + ) + val c = (url.openConnection() as HttpURLConnection).apply { + setRequestProperty("X-Bimba-Token", "ef0179272e7270e1a2da1710a8ba24e1") + } + c.inputStream + } +} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt b/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt new file mode 100644 index 0000000000000000000000000000000000000000..6cb63b72d138c72809b6d7fa963409d6a349d0ed --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/api/Responses.kt @@ -0,0 +1,51 @@ +package ml.adamsprogs.bimba.api + +import xyz.apiote.fruchtfleisch.Reader +import java.io.InputStream + +interface ItemsResponse { + companion object { + fun unmarshall(stream: InputStream): ItemsResponse { + val reader = Reader(stream) + when (reader.readUInt()) { + 0UL -> { + TODO("error response") + } + 1UL -> { + return ItemsSuccess.unmarshall(stream) + } + else -> { + TODO("throw unknown tag") + } + } + } + } +} + +data class ItemsSuccess(val items: List<Item>) : ItemsResponse { + companion object { + fun unmarshall(stream: InputStream): ItemsSuccess { + val items = mutableListOf<Item>() + val reader = Reader(stream) + val itemsNum = reader.readUInt() + for (i in 0UL until itemsNum) { + when (reader.readUInt()) { + 0UL -> { + items.add(Stop.unmarshall(stream)) + } + 1UL -> { + items.add(Line.unmarshall(stream)) + } + else -> { + TODO("throw unknown tag") + } + } + } + return ItemsSuccess(items) + } + } +} + +data class Error(val message: String) : ItemsResponse, DeparturesResponse { + +} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/search/Results.kt b/app/src/main/java/ml/adamsprogs/bimba/search/Results.kt new file mode 100644 index 0000000000000000000000000000000000000000..2218389bd2574588d9b6f66944be39a319137f43 --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/search/Results.kt @@ -0,0 +1,142 @@ +package ml.adamsprogs.bimba.search + +import android.content.Context +import android.graphics.PorterDuff +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources +import androidx.recyclerview.widget.RecyclerView +import com.mancj.materialsearchbar.adapter.SuggestionsAdapter +import ml.adamsprogs.bimba.R +import ml.adamsprogs.bimba.api.Item +import ml.adamsprogs.bimba.api.Line +import ml.adamsprogs.bimba.api.LineType +import ml.adamsprogs.bimba.api.Stop + +class BimbaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val root: View = itemView.findViewById(R.id.suggestion) + val icon: ImageView = itemView.findViewById(R.id.suggestion_image) + val title: TextView = itemView.findViewById(R.id.suggestion_title) + val description: TextView = itemView.findViewById(R.id.suggestion_description) + + companion object { + fun bind( + item: Item, + holder: BimbaViewHolder?, + context: Context?, + onClickListener: (Item) -> Unit + ) { + when (item) { + is Stop -> { + holder?.icon?.setImageResource(R.drawable.ic_stop_black_24dp) // todo stop icon // todo dark theme colour + holder?.title?.text = "${item.name} [${item.code}]" + holder?.description?.text = + item.changeOptions.joinToString { "${it.line} » ${it.headsign}" } + } + is Line -> { + val icon = when (item.type) { + LineType.TRAM -> { + AppCompatResources.getDrawable(context!!, R.drawable.ic_circle_white_24dp)?.mutate() + } + LineType.BUS -> { + AppCompatResources.getDrawable( + context!!, + R.drawable.ic_square_white_24dp + )?.mutate() // todo bigger and squircle + } + else -> { + null + } + } + holder?.icon?.apply { + setImageDrawable(icon) + colorFilter = null + Log.v("Colour", "${item.name}: ${item.colour}, ${item.colour.toString(16)}") + setColorFilter(item.colour, PorterDuff.Mode.SRC_IN) // fixme check colours in TRAFFIC + } + holder?.title?.text = item.name + holder?.description?.text = + "${item.headsignsThere.joinToString { it }} «» ${item.headsignsBack.joinToString { it }}" + } + else -> TODO("Shouldn't happen") + } + holder?.root?.setOnClickListener { + onClickListener(item) + } + /*holder!!.root.setOnClickListener { + onSuggestionClickListener.onSuggestionClickListener(suggestion) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + holder.text.text = Html.fromHtml(suggestion.getBody(context), Html.FROM_HTML_MODE_LEGACY) + } else { + @Suppress("DEPRECATION") + holder.text.text = Html.fromHtml(suggestion.getBody(context)) + } + + holder.text.setTextColor(getColor(context, R.color.textDark)) + + val icon = getDrawable(suggestion.getIcon(), context) + icon.mutate() + icon.colorFilter = null + if (suggestion is StopSuggestion) + when (suggestion.zone) { + "A" -> icon.setColorFilter(getColor(context, R.color.zoneA), PorterDuff.Mode.SRC_IN) + "B" -> icon.setColorFilter(getColor(context, R.color.zoneB), PorterDuff.Mode.SRC_IN) + "C" -> icon.setColorFilter(getColor(context, R.color.zoneC), PorterDuff.Mode.SRC_IN) + else -> icon.setColorFilter(getColor(context, R.color.textDark), PorterDuff.Mode.SRC_IN) + } + else if (suggestion is LineSuggestion) { + icon.setColorFilter(suggestion.getColour(), PorterDuff.Mode.SRC_IN) + holder.icon.setBackgroundColor(suggestion.getBgColour()) + } + holder.icon.setImageDrawable(icon)*/ + } + } +} + +class BimbaResultsAdapter( + private val inflater: LayoutInflater, + private val context: Context?, + private var items: List<Item>, + private val onClickListener: ((Item) -> Unit) +) : + RecyclerView.Adapter<BimbaViewHolder>() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BimbaViewHolder { + val rowView = inflater.inflate(R.layout.suggestion, parent, false) + return BimbaViewHolder(rowView) + } + + override fun onBindViewHolder(holder: BimbaViewHolder, position: Int) { + BimbaViewHolder.bind(items[position], holder, context, onClickListener) + } + + override fun getItemCount(): Int = items.size + + fun update(items: List<Item>) { + this.items = items + notifyDataSetChanged() + } +} + +class BimbaSuggestionsAdapter( + inflater: LayoutInflater, + private val context: Context?, + private val onClickListener: ((Item) -> Unit) +) : + SuggestionsAdapter<Item, BimbaViewHolder>(inflater) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BimbaViewHolder { + val rowView = layoutInflater.inflate(R.layout.suggestion, parent, false) + return BimbaViewHolder(rowView) + } + + override fun getSingleViewHeight(): Int = 64 + + override fun onBindSuggestionHolder(item: Item, holder: BimbaViewHolder?, pos: Int) { + BimbaViewHolder.bind(item, holder, context, onClickListener) + } +} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..57b5d1f384f54b620c1238774cf8a5846ab228f1 --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/search/ResultsActivity.kt @@ -0,0 +1,127 @@ +package ml.adamsprogs.bimba.search + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowCompat +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.openlocationcode.OpenLocationCode +import kotlinx.coroutines.* +import ml.adamsprogs.bimba.departures.DeparturesActivity +import ml.adamsprogs.bimba.api.* +import ml.adamsprogs.bimba.databinding.ActivityResultsBinding +import java.io.InputStream + +@SuppressLint("MissingPermission") +class ResultsActivity : AppCompatActivity(), LocationListener { + enum class Mode { + MODE_LOCATION, MODE_SEARCH + } + + private var _binding: ActivityResultsBinding? = null + + private val binding get() = _binding!! + + private lateinit var adapter: BimbaResultsAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + _binding = ActivityResultsBinding.inflate(layoutInflater) + setContentView(binding.root) + binding.resultsRecycler.layoutManager = LinearLayoutManager(this) + adapter = BimbaResultsAdapter(layoutInflater, this, listOf()) { + when (it) { + is Stop -> { + val intent = Intent(this, DeparturesActivity::class.java).apply { + putExtra("code", it.code) + putExtra("name", it.name) + // todo line, date, etc + } + startActivity(intent) + } + is Line -> { + TODO("start line graph actvity") + } + } + } + binding.resultsRecycler.adapter = adapter + setSupportActionBar(binding.topAppBar) + + WindowCompat.setDecorFitsSystemWindows(window, false) + + when (intent.extras?.get("mode")) { + Mode.MODE_LOCATION -> { + supportActionBar?.title = "Stops nearby" + locate() + } + Mode.MODE_SEARCH -> { + val query = intent.extras?.getString("query")!! + supportActionBar?.title = "Results for ‘$query’" + getItemsByQuery(query) + } + } + } + + private fun locate() { // todo also on back to this activity (onRestart?) + val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, 5000, 10f, this + ) + } + + override fun onLocationChanged(location: Location) { + val code = OpenLocationCode.encode(location.latitude, location.longitude) + val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager + locationManager.removeUpdates(this) + getItemsByLocation(code) + } + + override fun onDestroy() { // todo also on hide this activity (onPause?) + super.onDestroy() + val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager + locationManager.removeUpdates(this) + } + + private fun getItemsByQuery(query: String) { + MainScope().launch { + val itemsStream = queryItems(query) + updateItems(unmarshallItemResponse(itemsStream)) + Log.v("RESPONSE", "getItemsByQuery") + } + } + + private fun getItemsByLocation(plusCode: String) { + MainScope().launch { + val itemsStream = locateItems(plusCode) + updateItems(unmarshallItemResponse(itemsStream)) + Log.v("RESPONSE", "getItemsByLocation") + } + } + + private fun updateItems(items: List<Item>) { + binding.resultsProgress.visibility = View.GONE + binding.resultsRecycler.visibility = View.VISIBLE + adapter.update(items) + } + + private suspend fun unmarshallItemResponse(stream: InputStream): List<Item> { + return withContext(Dispatchers.IO) { + when (val response = ItemsResponse.unmarshall(stream)) { + is ItemsSuccess -> { + return@withContext response.items + } + else -> { + TODO("Error response") + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_circle_white_24dp.xml b/app/src/main/res/drawable/ic_circle_white_24dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..5aaede4f20494018750d6c4d5403633bdba10779 --- /dev/null +++ b/app/src/main/res/drawable/ic_circle_white_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@android:color/white" + android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_square_white_24dp.xml b/app/src/main/res/drawable/ic_square_white_24dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..c3963cf6bf7715f31cfd1f5e9c5ebce6fcff0029 --- /dev/null +++ b/app/src/main/res/drawable/ic_square_white_24dp.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#FFFFFF" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M6,6h12v12H6z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_stop_black_24dp.xml b/app/src/main/res/drawable/ic_stop_black_24dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..14756c18e895b90db0ddfeddf4d937b7471c367f --- /dev/null +++ b/app/src/main/res/drawable/ic_stop_black_24dp.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#000000" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M2,12C2,6.48 6.48,2 12,2s10,4.48 10,10 -4.48,10 -10,10S2,17.52 2,12zM12,18c3.31,0 6,-2.69 6,-6s-2.69,-6 -6,-6 -6,2.69 -6,6 2.69,6 6,6z"/> +</vector> diff --git a/app/src/main/res/layout/activity_results.xml b/app/src/main/res/layout/activity_results.xml new file mode 100644 index 0000000000000000000000000000000000000000..52fbb1021ffa1df3b2e05915a803979f8160c9fa --- /dev/null +++ b/app/src/main/res/layout/activity_results.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/results_progress" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ProgressBar + style="?android:attr/progressBarStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + <com.google.android.material.appbar.AppBarLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fitsSystemWindows="true" + app:liftOnScroll="true"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/topAppBar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + app:title="@string/title_activity_results" /> + + </com.google.android.material.appbar.AppBarLayout> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/results_recycler" + android:layout_width="match_parent" + android:layout_height="match_parent" + + android:visibility="gone" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> +</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/suggestion.xml b/app/src/main/res/layout/suggestion.xml new file mode 100644 index 0000000000000000000000000000000000000000..d1eb7758fad295a2a8d7fe4f1c54638ea9a67cef --- /dev/null +++ b/app/src/main/res/layout/suggestion.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/suggestion" + android:layout_width="match_parent" + android:layout_height="64dp"> + + <ImageView + android:id="@+id/suggestion_image" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/suggestion_title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="8dp" + android:text="" + android:textAppearance="@style/Theme.Bimba.SearchResult.Title" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/suggestion_image" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/suggestion_description" + style="@style/Theme.Bimba.SearchResult.Description" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginTop="4dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="8dp" + android:text="" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/suggestion_title" + app:layout_constraintTop_toBottomOf="@+id/suggestion_title" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file