Skip to content

Commit

Permalink
Implement new MapScreen using MapWithContentTemplate
Browse files Browse the repository at this point in the history
  • Loading branch information
johan12345 committed Jun 30, 2024
1 parent 44a4060 commit ad15cae
Show file tree
Hide file tree
Showing 14 changed files with 1,554 additions and 480 deletions.
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />

Expand Down
8 changes: 6 additions & 2 deletions app/src/main/java/net/vonforst/evmap/auto/CarAppService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,12 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
}

override fun onCreateScreen(intent: Intent): Screen {

val mapScreen = MapScreen(carContext, this)
val mapScreen =
if (carContext.carAppApiLevel >= 7 && carContext.isAppDrivenRefreshSupported) {
MapScreen(carContext, this)
} else {
LegacyMapScreen(carContext, this)
}
val screens = mutableListOf<Screen>(mapScreen)

handleActionsIntent(intent)?.let {
Expand Down
67 changes: 2 additions & 65 deletions app/src/main/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package net.vonforst.evmap.auto
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
Expand All @@ -13,7 +12,6 @@ import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.HostException
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action
Expand Down Expand Up @@ -53,9 +51,6 @@ import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.fronyx.FronyxApi
import net.vonforst.evmap.api.fronyx.PredictionData
import net.vonforst.evmap.api.fronyx.PredictionRepository
import net.vonforst.evmap.api.iconForPlugType
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Cost
import net.vonforst.evmap.model.FaultReport
Expand All @@ -65,7 +60,6 @@ import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.awaitFinished
Expand Down Expand Up @@ -135,7 +129,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
.setFlags(Action.FLAG_PRIMARY)
.setBackgroundColor(CarColor.PRIMARY)
.setOnClickListener {
navigateToCharger(charger)
navigateToCharger(carContext, charger)
}
.build())
if (ChargepriceApi.isChargerSupported(charger)) {
Expand Down Expand Up @@ -272,7 +266,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
Row.IMAGE_TYPE_LARGE
)
}
addText(generateChargepointsText(charger))
addText(generateChargepointsText(charger, availability, carContext))
}.build())
if (maxRows <= 3) {
// row 2: operator + cost + fault report
Expand Down Expand Up @@ -485,47 +479,6 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
return string
}

private fun generateChargepointsText(charger: ChargeLocation): SpannableStringBuilder {
val chargepointsText = SpannableStringBuilder()
charger.chargepointsMerged.forEachIndexed { i, cp ->
chargepointsText.apply {
if (i > 0) append(" · ")
append("${cp.count}× ")
val plugIcon = iconForPlugType(cp.type)
if (plugIcon != 0) {
append(
nameForPlugType(carContext.stringProvider(), cp.type),
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
plugIcon
)
).setTint(
CarColor.createCustom(Color.WHITE, Color.BLACK)
).build()
),
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
} else {
append(nameForPlugType(carContext.stringProvider(), cp.type))
}
cp.formatPower()?.let {
append(" ")
append(it)
}
}
availability?.status?.get(cp)?.let { status ->
chargepointsText.append(
" (${availabilityText(status)}/${cp.count})",
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
return chargepointsText
}

private fun generateOperatorText(charger: ChargeLocation) =
if (charger.operator != null && charger.network != null) {
if (charger.operator.contains(charger.network)) {
Expand All @@ -543,22 +496,6 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
carContext.getString(R.string.unknown_operator)
}

private fun navigateToCharger(charger: ChargeLocation) {
val coord = charger.coordinates
val intent =
Intent(
CarContext.ACTION_NAVIGATE,
Uri.parse("geo:${coord.lat},${coord.lng}")
)
try {
carContext.startCarApp(intent)
} catch (e: HostException) {
CarToast.makeText(carContext, R.string.no_maps_app_found, CarToast.LENGTH_SHORT).show()
} catch (ignored: SecurityException) {

}
}

private fun loadCharger() {
lifecycleScope.launch {
favorite = db.favoritesDao().findFavorite(chargerSparse.id, chargerSparse.dataSource)
Expand Down
242 changes: 242 additions & 0 deletions app/src/main/java/net/vonforst/evmap/auto/ChargerListFormatter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package net.vonforst.evmap.auto

import android.location.Location
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.car.app.CarContext
import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.model.Action
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarIconSpan
import androidx.car.app.model.CarLocation
import androidx.car.app.model.CarText
import androidx.car.app.model.DistanceSpan
import androidx.car.app.model.ForegroundCarColorSpan
import androidx.car.app.model.ItemList
import androidx.car.app.model.Metadata
import androidx.car.app.model.Pane
import androidx.car.app.model.Place
import androidx.car.app.model.PlaceMarker
import androidx.car.app.model.Row
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.utils.distanceBetween
import java.time.ZonedDateTime
import kotlin.math.roundToInt

interface ChargerListDelegate : ItemList.OnItemVisibilityChangedListener {
val locationError: Boolean
val loadingError: Boolean
val maxRows: Int
val filterStatus: Long
val location: Location?
val energyLevel: EnergyLevel?
fun onChargerClick(charger: ChargeLocation)
}

class ChargerListFormatter(val carContext: CarContext, val screen: ChargerListDelegate) {
private val iconGen = ChargerIconGenerator(carContext, null, height = 96)
var favorites: Set<Long> = emptySet()

fun buildChargerList(
chargers: List<ChargeLocation>?,
availabilities: Map<Long, Pair<ZonedDateTime, ChargeLocationStatus?>>
): ItemList? {
return if (chargers != null) {
val chargerList = chargers.take(screen.maxRows)
val builder = ItemList.Builder()
// only show the city if not all chargers are in the same city
val showCity = chargerList.map { it.address?.city }.distinct().size > 1
chargerList.forEach { charger ->
builder.addItem(
formatCharger(
charger,
availabilities,
showCity,
charger.id in favorites
)
)
}
builder.setNoItemsMessage(
carContext.getString(
if (screen.filterStatus == FILTERS_FAVORITES) {
R.string.auto_no_favorites_found
} else {
R.string.auto_no_chargers_found
}
)
)
builder.setOnItemsVisibilityChangedListener(screen)
builder.build()
} else {
if (screen.loadingError) {
val builder = ItemList.Builder()
builder.setNoItemsMessage(
carContext.getString(R.string.connection_error)
)
builder.build()
} else if (screen.locationError) {
val builder = ItemList.Builder()
builder.setNoItemsMessage(
carContext.getString(R.string.location_error)
)
builder.build()
} else {
null
}
}
}

private fun formatCharger(
charger: ChargeLocation,
availabilities: Map<Long, Pair<ZonedDateTime, ChargeLocationStatus?>>,
showCity: Boolean,
isFavorite: Boolean
): Row {
val markerTint = getMarkerTint(charger)
val backgroundTint = if ((charger.maxPower ?: 0.0) > 100) {
R.color.charger_100kw_dark // slightly darker color for better contrast
} else {
markerTint
}
val color = ContextCompat.getColor(carContext, backgroundTint)
val place =
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
.setMarker(
PlaceMarker.Builder()
.setColor(CarColor.createCustom(color, color))
.build()
)
.build()

val icon = iconGen.getBitmap(
markerTint,
fault = charger.faultReport != null,
multi = charger.isMulti(),
fav = isFavorite
)
val iconSpan =
CarIconSpan.create(CarIcon.Builder(IconCompat.createWithBitmap(icon)).build())

return Row.Builder().apply {
// only show the city if not all chargers are in the same city (-> showCity == true)
// and the city is not already contained in the charger name
val title = SpannableStringBuilder().apply {
append(" ", iconSpan, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
append(" ")
append(charger.name)
}
if (showCity && charger.address?.city != null && charger.address.city !in charger.name) {
val titleWithCity = SpannableStringBuilder().apply {
append("", iconSpan, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
append(" ")
append("${charger.name} · ${charger.address.city}")
}
setTitle(CarText.Builder(titleWithCity).addVariant(title).build())
} else {
setTitle(title)
}

val text = SpannableStringBuilder()

// distance
screen.location?.let {
val distanceMeters = distanceBetween(
it.latitude, it.longitude,
charger.coordinates.lat, charger.coordinates.lng
)
text.append(
"distance",
DistanceSpan.create(
roundValueToDistance(
distanceMeters,
screen.energyLevel?.distanceDisplayUnit?.value,
carContext
)
),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}

// power
val power = charger.maxPower
if (power != null) {
if (text.isNotEmpty()) text.append(" · ")
text.append("${power.roundToInt()} kW")
}

// availability
availabilities[charger.id]?.second?.let { av ->
val status = av.status.values.flatten()
val available = availabilityText(status)
val total = charger.chargepoints.sumOf { it.count }

if (text.isNotEmpty()) text.append(" · ")
text.append(
"$available/$total",
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}

addText(text)
setMetadata(
Metadata.Builder()
.setPlace(place)
.build()
)

setOnClickListener {
screen.onChargerClick(charger)
}
}.build()
}

fun buildSingleCharger(
charger: ChargeLocation,
availability: ChargeLocationStatus?,
onClick: () -> Unit
) = Pane.Builder().apply {
val icon = iconGen.getBitmap(
getMarkerTint(charger),
fault = charger.faultReport != null,
multi = charger.isMulti(),
fav = charger.id in favorites
)


addRow(Row.Builder().apply {
setImage(CarIcon.Builder(IconCompat.createWithBitmap(icon)).build())
setTitle(charger.address.toString())
addText(generateChargepointsText(charger, availability, carContext))
}.build())
addAction(Action.Builder().apply {
setTitle(carContext.getString(R.string.show_more))
setOnClickListener(onClick)
}.build())
addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_navigation
)
).build()
)
setTitle(carContext.getString(R.string.navigate))
setBackgroundColor(CarColor.PRIMARY)
setOnClickListener {
navigateToCharger(carContext, charger)
}
}.build())
}.build()
}
Loading

0 comments on commit ad15cae

Please sign in to comment.