This commit is contained in:
Dimitris
2026-04-28 16:52:48 +02:00
parent 42b3eabe47
commit ae4d3f321e
17 changed files with 268 additions and 50 deletions
+2 -2
View File
@@ -17,8 +17,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 37
versionCode = 103
versionName = "0.3.0.103"
versionCode = 104
versionName = "0.3.0.104"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -16,6 +16,7 @@ import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
@@ -60,6 +61,7 @@ import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
import org.maplibre.geojson.FeatureCollection
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
@@ -116,6 +118,8 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
var lastAlert : Long = 0
var navigationManagerStarted = false
var updateLocationIndex = 0
/**
* Lifecycle observer for managing session lifecycle events.
* Cleans up resources when the session is destroyed.
@@ -438,6 +442,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
surfaceRenderer.updateLocation(location, streetName)
}
updateLocationIndex++
}
/**
@@ -471,7 +476,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
checkArrival()
}
val endTime = System.currentTimeMillis() - startTime
//Log.d(TAG, "handleNavigationLocation: $endTime")
Log.d(TAG, "handleNavigationLocation: $endTime")
}
/**
@@ -507,14 +512,14 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
return
}
val stepData = routeModel.currentStep() // 121 ms
val stepData = routeModel.currentStep()
navigationScreen.updateTrip( // 277 ms
navigationScreen.updateTrip(
isNavigating = routeModel.isNavigating(),
isRerouting = false,
hasArrived = routeModel.isArrival(),
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext), // 60 ms
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext), // 60 ms
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext),
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext),
destinations = mutableListOf(routeModel.getDestination()),
steps = routeModel.getSteps(carContext),
stepRemainingDistance = routeModel.getDistance(),
@@ -529,7 +534,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
* Updates the trip information and notifies the listener with a new Trip object.
* This includes destination name, address, travel estimate, and loading status.
*/
updateTrip(routeModel.getTrip(carContext)) // 230 ms
updateTrip(routeModel.getTrip(carContext))
}
/**
@@ -703,6 +708,10 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
navigationScreen.invalidate()
}
override fun onTrafficMessageReceived(trafficMessage: String) {
}
/**
* Loads a route to the specified place and sets it as the destination.
*/
@@ -748,6 +757,12 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
lastTrafficDate = current
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
if (updateLocationIndex % 4 == 0) {
// navigationViewModel.updateTrafficMessages(
// surfaceRenderer.lastLocation,
// surfaceRenderer.trafficData.value!!
// )
}
}
/**
@@ -1,6 +1,7 @@
package com.kouros.navigation.car.map
import android.location.Location
import android.util.Log
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
@@ -28,12 +29,15 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.kouros.data.R
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.NavigationColorDark
import com.kouros.navigation.data.NavigationColorLight
import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.utils.GeoUtils.createEndCollection
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.isMetricSystem
import com.kouros.navigation.utils.location
import org.maplibre.compose.camera.CameraPosition
@@ -47,6 +51,8 @@ import org.maplibre.compose.expressions.dsl.interpolate
import org.maplibre.compose.expressions.dsl.zoom
import org.maplibre.compose.expressions.value.ColorValue
import org.maplibre.compose.expressions.value.DpValue
import org.maplibre.compose.expressions.value.ExpressionValue
import org.maplibre.compose.expressions.value.ImageValue
import org.maplibre.compose.layers.Anchor
import org.maplibre.compose.layers.FillLayer
import org.maplibre.compose.layers.LineLayer
@@ -61,6 +67,8 @@ import org.maplibre.compose.sources.Source
import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.sources.rememberGeoJsonSource
import org.maplibre.compose.style.BaseStyle
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.LineString
import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds
@@ -131,6 +139,7 @@ fun MapLibre(
fun StartEndLayer(routeData: String?) {
if (!routeData.isNullOrEmpty()) {
val end = createEndCollection(routeData)
Log.d("Map End", " $end")
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(end))
val img = image(painterResource(R.drawable.sports_score_48px), drawAsSdf = true)
SymbolLayer(
@@ -186,9 +195,49 @@ fun TrafficLayer(trafficData: Map<String, String>) {
color = trafficColor(it.key),
width = routeLineWidth(base = 2.dp, isCasing = false),
)
val featureCollection = FeatureCollection.fromJson(it.value)
if (featureCollection.features()!!.isNotEmpty()) {
val points = mutableListOf<List<Double>>()
featureCollection.features()!!.forEach { geo ->
val coordinates = (geo.geometry() as LineString).coordinates()
if (coordinates.size > 5) {
val first = coordinates.first()
val second = coordinates[1]
points.add(listOf(first.coordinates()[0], second.coordinates()[1]))
}
}
val collection = createPointCollection(
points, it.key
)
TrafficPoint(it.key, "${it.key}-point-layer", collection)
}
}
}
@Composable
fun TrafficPoint(id: String, layer: String, routeData: String?) {
if (!routeData.isNullOrEmpty()) {
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
val img = trafficImage(id)
SymbolLayer(
id = layer,
source = routes,
iconColor = const(Color.Red),
iconImage = img,
iconSize =
interpolate(
type = exponential(2f),
input = zoom(),
5 to const(0.4f),
6 to const(1.6f),
7 to const(2.0f),
20 to const(5.0f),
),
)
}
}
/**
* Reusable helper for consistent line widths across all route layers.
*/
@@ -198,12 +247,12 @@ private fun routeLineWidth(base: Dp, isCasing: Boolean = false): Expression<DpVa
val width = base + extra
return interpolate(
type = exponential(1.1f),
type = exponential(2f),
input = zoom(),
5 to const(width + 3.dp),
10 to const(width + 8.dp),
1 to const(width + 3.dp),
15 to const(width + 13.dp),
20 to const(width + 18.dp),
18 to const(width + 15.dp),
24 to const(width + 24.dp),
)
}
@@ -232,6 +281,16 @@ fun RouteLayerPoint(routeData: String?) {
}
@Composable
fun trafficImage(key: String): Expression<ImageValue> {
when (key) {
"heavy", "queuing", "slow", "stationary" -> return image(painterResource(R.drawable.traffic_jam_24px), drawAsSdf = true)
"roadworks" -> return image(painterResource(R.drawable.construction_24px), drawAsSdf = true)
"closed", "lane" -> return image(painterResource(R.drawable.remove_road_24px), drawAsSdf = true)
}
return image(painterResource(R.drawable.traffic_jam_24px), drawAsSdf = true)
}
fun trafficColor(key: String): Expression<ColorValue> {
when (key) {
"queuing" -> return const(Color(0xFFC46E53))
@@ -239,6 +298,8 @@ fun trafficColor(key: String): Expression<ColorValue> {
"stationary" -> return const(Color(0xFF910A0A))
"heavy" -> return const(Color(0xFF6B0404))
"roadworks" -> return const(Color(0xFFDAA707))
"closed" -> return const(Color(0xFFDA0727))
"lane" -> return const(Color(0xFFEFBAC1))
}
return const(Color.Blue)
}
@@ -29,4 +29,6 @@ interface NavigationObserverCallback {
/** Called to request UI invalidation/refresh */
fun invalidateScreen()
fun onTrafficMessageReceived(trafficMessage: String)
}
@@ -14,6 +14,8 @@ class NavigationObserverManager(
val routeObserver = RouteObserver(callback)
val trafficObserver = TrafficObserver(callback)
val trafficMessageObserver = TrafficMessageObserver(callback)
val placeSearchObserver = PlaceSearchObserver(callback)
val speedCameraObserver = SpeedCameraObserver(callback)
val maxSpeedObserver = MaxSpeedObserver(callback)
@@ -21,6 +23,7 @@ class NavigationObserverManager(
fun attachAllObservers(session: NavigationSession) {
viewModel.route.observe(session, routeObserver)
viewModel.traffic.observe(session, trafficObserver)
viewModel.trafficMessage.observe(session, trafficMessageObserver)
viewModel.placeLocation.observe(session, placeSearchObserver)
viewModel.speedCameras.observe(session, speedCameraObserver)
viewModel.maxSpeed.observe(session, maxSpeedObserver)
@@ -0,0 +1,16 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
/**
* Observer for traffic data updates.
*/
class TrafficMessageObserver(
private val callback: NavigationObserverCallback
) : Observer<String> {
override fun onChanged(value: String) {
callback.onTrafficMessageReceived(value)
callback.invalidateScreen()
}
}
@@ -0,0 +1,44 @@
package com.kouros.navigation.data
import android.util.Log
import java.io.File
data class ApplicationConfig(
val user: String,
val password: String
) {
companion object {
fun load(): ApplicationConfig {
fun Map<String, String>.envOrLookup(key: String): String {
return System.getenv(key) ?: this[key]!!
}
val envVars: Map<String, String> = envFile().let { envFile ->
if (envFile.exists()) {
envFile.readLines()
.map { it.split("=") }
.filter { it.size == 2 }
.associate { it.first().trim() to it.last().trim() }
} else emptyMap()
}
return ApplicationConfig(
user = envVars.envOrLookup("USER"),
password = envVars.envOrLookup("PASSWORD"),
)
}
}
}
fun envFile(): File {
//val path = context.filesDir.absolutePath
//val config = ApplicationConfig.load(context)
return listOf(".env").map {
Log.d("Overpass", it)
File(it)
}.first { it.exists() }
}
@@ -3,8 +3,7 @@ package com.kouros.navigation.data
import androidx.compose.ui.graphics.Color
val NavigationColorLight = Color(0xFF17A119)
val NavigationColorDark = Color(0xFF07568C)
val NavigationColorDark = Color(0xFF413EAD)
val RouteColor = Color(0xFF5201B4)
@@ -146,6 +146,8 @@ object Constants {
const val TRAFFIC_UPDATE = 300
const val TRAFFIC_MESSAGE_UPDATE = 10
const val SPEED_UPDATE_DISTANCE = 600F
const val INSTRUCTION_DISTANCE = 50
@@ -4,9 +4,12 @@ import android.location.Location
import android.util.Log
import com.google.gson.GsonBuilder
import com.kouros.data.BuildConfig
import com.kouros.navigation.data.ApplicationConfig
import com.kouros.navigation.utils.GeoUtils.getBoundingBox
import java.io.OutputStreamWriter
import java.net.Authenticator
import java.net.HttpURLConnection
import java.net.PasswordAuthentication
import java.net.URL
class Overpass {
@@ -94,7 +97,17 @@ class Overpass {
}
private fun overpassApi(connection: HttpURLConnection, searchQuery: String): List<Elements> {
return try {
Authenticator.setDefault(object : Authenticator() {
override fun getPasswordAuthentication(): PasswordAuthentication {
return PasswordAuthentication(
"kouros",
"eo7sbjyWpmjSVFyELgbfrryqJ6ddNeq9".toCharArray()
)
}
})
connection.outputStream.use { os ->
OutputStreamWriter(os).use { writer ->
writer.write(searchQuery)
@@ -23,9 +23,10 @@ private const val tomtomFields =
val useLocal = BuildConfig.DEBUG
val useLocalTraffic = BuildConfig.DEBUG
val useLocalTraffic = BuildConfig.DEBUG
class TomTomRepository : NavigationRepository() {
override fun getRoute(
context: Context,
currentLocation: Location,
@@ -102,7 +103,7 @@ class TomTomRepository : NavigationRepository() {
if (!showTraffic) {
return ""
}
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
val bbox = calculateSquareRadius(location.latitude, location.longitude, 10.0)
return if (useLocalTraffic) {
fetchUrl(
"http://192.168.1.37/tomtom_traffic.json",
@@ -3,7 +3,6 @@ package com.kouros.navigation.model
import android.content.Context
import android.location.Location
import android.util.Log
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.MutableLiveData
@@ -11,9 +10,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.kouros.navigation.data.ApplicationConfig
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.SPEED_BEARING_DEVIATION
import com.kouros.navigation.data.Constants.SPEED_UPDATE_DISTANCE
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Places
@@ -36,6 +37,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.LineString
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.collections.first
@@ -59,6 +61,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
MutableLiveData()
}
/** LiveData containing categorized traffic messages map */
val trafficMessage: MutableLiveData<String> by lazy {
MutableLiveData()
}
/** LiveData containing a preview route JSON string for route preview screens */
val previewRoute: MutableLiveData<String> by lazy {
MutableLiveData()
@@ -223,33 +230,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Categorizes traffic incidents by type (queuing, stationary, slow, heavy, roadworks).
* @param data Raw traffic GeoJSON string
* @return Map of incident type to GeoJSON FeatureCollection
*/
private fun rebuildTraffic(data: String): Map<String, String> {
val featureCollection = FeatureCollection.fromJson(data)
val incidents = mutableMapOf<String, String>()
val queuing = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Queuing traffic") }
incidents["queuing"] = FeatureCollection.fromFeatures(queuing).toJson()
val stationary = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Stationary traffic") }
incidents["stationary"] = FeatureCollection.fromFeatures(stationary).toJson()
val slow = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Slow traffic") }
incidents["slow"] = FeatureCollection.fromFeatures(slow).toJson()
val heavy = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Heavy traffic") }
incidents["heavy"] = FeatureCollection.fromFeatures(heavy).toJson()
val roadworks = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Roadworks") }
incidents["roadworks"] = FeatureCollection.fromFeatures(roadworks).toJson()
return incidents
}
/**
* Calculates a preview route for route preview screen.
* Posts the route JSON to previewRoute LiveData.
@@ -278,6 +258,40 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return previewRoute.value
}
/**
* Categorizes traffic incidents by type (queuing, stationary, slow, heavy, roadworks).
* @param data Raw traffic GeoJSON string
* @return Map of incident type to GeoJSON FeatureCollection
*/
private fun rebuildTraffic(data: String): Map<String, String> {
val featureCollection = FeatureCollection.fromJson(data)
val incidents = mutableMapOf<String, String>()
val queuing = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Queuing traffic") }
incidents["queuing"] = FeatureCollection.fromFeatures(queuing).toJson()
val stationary = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Stationary traffic") }
incidents["stationary"] = FeatureCollection.fromFeatures(stationary).toJson()
val slow = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Slow traffic") }
incidents["slow"] = FeatureCollection.fromFeatures(slow).toJson()
val heavy = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Heavy traffic") }
incidents["heavy"] = FeatureCollection.fromFeatures(heavy).toJson()
val roadworks = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Roadworks") }
incidents["roadworks"] = FeatureCollection.fromFeatures(roadworks).toJson()
val laneClosed = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Lane closed") }
incidents["lane"] = FeatureCollection.fromFeatures(laneClosed).toJson()
val closed = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Closed") }
incidents["closed"] = FeatureCollection.fromFeatures(closed).toJson()
return incidents
}
/**
* Loads device contacts with addresses and converts to Place objects.
@@ -434,17 +448,17 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
var element: Elements?
val search = mutableListOf<ElementSearch>()
speedElements.filter { it.type == "way"}.forEach {
speedElements.filter { it.type == "way" }.forEach {
var distance: Float
var maxDistance = 1000F
var geometryFirstLocation = location(0.0, 0.0)
var geometryLastLocation = location(0.0, 0.0)
for ((geoIndex, geo) in it.geometry.withIndex()) {
if (geoIndex == 0) {
geometryFirstLocation= location(geo.lon, geo.lat)
geometryFirstLocation = location(geo.lon, geo.lat)
}
if (geoIndex == it.geometry.size-1) {
geometryLastLocation = location(geo.lon, geo.lat)
if (geoIndex == it.geometry.size - 1) {
geometryLastLocation = location(geo.lon, geo.lat)
}
val geometryLocation = location(geo.lon, geo.lat)
distance = geometryLocation.distanceTo(location)
@@ -463,7 +477,8 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
)
}
}
val result = search.sortedWith(compareBy<ElementSearch> { it.distance }.thenByDescending { it.bearing })
val result =
search.sortedWith(compareBy<ElementSearch> { it.distance }.thenByDescending { it.bearing })
if (result.isNotEmpty()) {
element = result.first().element
speed = if (element.tags.maxspeed == "none" && element.tags.highway == "motorway") {
@@ -479,7 +494,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return speed
}
private fun isBearingValid(element: Elements, streetBearing : Float, routeBearing: Float) : Boolean {
private fun isBearingValid(
element: Elements,
streetBearing: Float,
routeBearing: Float
): Boolean {
return if (element.tags.oneway.isNotEmpty()) {
(streetBearing - routeBearing.absoluteValue) < SPEED_BEARING_DEVIATION
} else {
@@ -1,7 +1,9 @@
package com.kouros.navigation.model
import android.location.Location
import android.util.Log
import androidx.car.app.navigation.model.Step
import com.kouros.navigation.data.ApplicationConfig
import com.kouros.navigation.data.Constants.MAXIMUM_LOCATION_DISTANCE
import com.kouros.navigation.data.Constants.NEAREST_LOCATION_DISTANCE
import com.kouros.navigation.data.Constants.SPEED_UPDATE_DISTANCE
@@ -117,6 +119,7 @@ class RouteCalculator(var routeModel: RouteModel) {
if ((distance > SPEED_UPDATE_DISTANCE * 2) || lastSpeedIndex < routeModel.route.currentStepIndex) {
lastSpeedIndex = routeModel.route.currentStepIndex
lastSpeedLocation = location
viewModel.updateSpeedLimit(
location,
routeModel.route.currentStep().street,
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M620,680Q645,680 662.5,662.5Q680,645 680,620Q680,595 662.5,577.5Q645,560 620,560Q595,560 577.5,577.5Q560,595 560,620Q560,645 577.5,662.5Q595,680 620,680ZM260,680Q285,680 302.5,662.5Q320,645 320,620Q320,595 302.5,577.5Q285,560 260,560Q235,560 217.5,577.5Q200,595 200,620Q200,645 217.5,662.5Q235,680 260,680ZM680,480Q597,480 538.5,421.5Q480,363 480,280Q480,198 538,139Q596,80 680,80Q763,80 821.5,138.5Q880,197 880,280Q880,363 821.5,421.5Q763,480 680,480ZM660,320L700,320L700,160L660,160L660,320ZM680,400Q688,400 694,394Q700,388 700,380Q700,372 694,366Q688,360 680,360Q672,360 666,366Q660,372 660,380Q660,388 666,394Q672,400 680,400ZM120,880Q103,880 91.5,868.5Q80,857 80,840L80,520L164,280Q170,262 185.5,251Q201,240 220,240L403,240Q400,260 400,280Q400,300 403,320L234,320L192,440L451,440Q491,497 551,528.5Q611,560 680,560Q711,560 741.5,553.5Q772,547 800,533L800,840Q800,857 788.5,868.5Q777,880 760,880L720,880Q703,880 691.5,868.5Q680,857 680,840L680,800L200,800L200,840Q200,857 188.5,868.5Q177,880 160,880L120,880Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M756,840L537,621L621,537L840,756L756,840ZM204,840L120,756L396,480L328,412L300,440L249,389L249,471L221,499L100,378L128,350L210,350L160,300L302,158Q322,138 345,129Q368,120 392,120Q416,120 439,129Q462,138 482,158L390,250L440,300L412,328L480,396L570,306Q566,295 563.5,283Q561,271 561,259Q561,200 601.5,159.5Q642,119 701,119Q716,119 729.5,122Q743,125 757,131L658,230L730,302L829,203Q836,217 838.5,230.5Q841,244 841,259Q841,318 800.5,358.5Q760,399 701,399Q689,399 677,397Q665,395 654,390L204,840Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M720,520L720,160L800,160L800,520L720,520ZM160,800L160,160L240,160L240,800L160,800ZM440,320L440,160L520,160L520,320L440,320ZM440,560L440,400L520,400L520,560L440,560ZM440,800L440,640L520,640L520,800L440,800ZM617,823L702,738L617,654L674,597L759,682L844,597L900,654L815,739L899,824L844,880L758,795L673,880L617,823Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M80,880Q63,880 51.5,868.5Q40,857 40,840L40,520L125,317Q132,300 147,290Q162,280 180,280L540,280Q558,280 573,290Q588,300 595,317L680,520L680,840Q680,857 668.5,868.5Q657,880 640,880L600,880Q583,880 571.5,868.5Q560,857 560,840L560,800L160,800L160,840Q160,857 148.5,868.5Q137,880 120,880L80,880ZM152,440L567,440L534,360L186,360L152,440ZM262.5,662.5Q280,645 280,620Q280,595 262.5,577.5Q245,560 220,560Q195,560 177.5,577.5Q160,595 160,620Q160,645 177.5,662.5Q195,680 220,680Q245,680 262.5,662.5ZM542.5,662.5Q560,645 560,620Q560,595 542.5,577.5Q525,560 500,560Q475,560 457.5,577.5Q440,595 440,620Q440,645 457.5,662.5Q475,680 500,680Q525,680 542.5,662.5ZM720,760L720,416L647,240L227,240L245,197Q252,180 267,170Q282,160 300,160L660,160Q678,160 693,170Q708,180 715,197L800,400L800,720Q800,737 788.5,748.5Q777,760 760,760L720,760ZM840,640L840,296L767,120L347,120L365,77Q372,60 387,50Q402,40 420,40L780,40Q798,40 813,50Q828,60 835,77L920,280L920,600Q920,617 908.5,628.5Q897,640 880,640L840,640Z"/>
</vector>