diff --git a/app/src/google/AndroidManifest.xml b/app/src/google/AndroidManifest.xml
index 0b26250d..b8dc6d13 100644
--- a/app/src/google/AndroidManifest.xml
+++ b/app/src/google/AndroidManifest.xml
@@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="net.vonforst.evmap">
-
+
@@ -36,5 +36,7 @@
android:name=".auto.CarLocationService"
android:foregroundServiceType="location"
android:enabled="true" />
+
+
\ No newline at end of file
diff --git a/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt b/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt
index e6632f69..f07f4713 100644
--- a/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt
+++ b/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt
@@ -1,11 +1,15 @@
package net.vonforst.evmap.auto
+import android.Manifest
import android.content.*
+import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.location.Location
import android.net.Uri
+import android.os.Bundle
import android.os.IBinder
+import android.os.ResultReceiver
import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.core.content.ContextCompat
@@ -32,6 +36,7 @@ import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.api.nameForPlugType
+import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.utils.distanceBetween
@@ -39,9 +44,17 @@ import java.time.Duration
import java.time.ZonedDateTime
import kotlin.math.roundToInt
+interface LocationAwareScreen {
+ fun updateLocation(location: Location)
+}
class CarAppService : com.google.android.libraries.car.app.CarAppService(), LifecycleObserver {
- private lateinit var mapScreen: MapScreen
+ var mapScreen: LocationAwareScreen? = null
+ set(value) {
+ field = value
+ location?.let { value?.updateLocation(it) }
+ }
+ private var location: Location? = null
private var locationService: CarLocationService? = null
private val serviceConnection = object : ServiceConnection {
@@ -60,23 +73,35 @@ class CarAppService : com.google.android.libraries.car.app.CarAppService(), Life
private val locationReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val location = intent.getParcelableExtra(CarLocationService.EXTRA_LOCATION) as Location?
- if (location != null) {
+ val mapScreen = this@CarAppService.mapScreen
+ if (location != null && mapScreen != null) {
mapScreen.updateLocation(location)
}
+ this@CarAppService.location = location
}
}
override fun onCreate() {
- mapScreen = MapScreen(carContext)
lifecycle.addObserver(this)
}
override fun onCreateScreen(intent: Intent): Screen {
- return mapScreen
+ return if (locationPermissionGranted()) {
+ WelcomeScreen(carContext, this)
+ } else {
+ PermissionScreen(carContext, this)
+ }
}
+ private fun locationPermissionGranted() =
+ ContextCompat.checkSelfPermission(
+ carContext,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+
@OnLifecycleEvent(Lifecycle.Event.ON_START)
- private fun bindLocationService() {
+ fun bindLocationService() {
+ if (!locationPermissionGranted()) return
bindService(
Intent(this, CarLocationService::class.java),
serviceConnection,
@@ -104,7 +129,118 @@ class CarAppService : com.google.android.libraries.car.app.CarAppService(), Life
}
}
-class MapScreen(ctx: CarContext) : Screen(ctx) {
+/**
+ * Welcome screen with selection between favorites and nearby chargers
+ */
+class WelcomeScreen(ctx: CarContext, val cas: CarAppService) : Screen(ctx), LocationAwareScreen {
+ private var location: Location? = null
+
+ override fun getTemplate(): Template {
+ cas.mapScreen = this
+ return PlaceListMapTemplate.builder().apply {
+ setTitle(carContext.getString(R.string.app_name))
+ location?.let {
+ setAnchor(Place.builder(LatLng.create(it)).build())
+ }
+ setItemList(ItemList.builder().apply {
+ addItem(Row.builder()
+ .setTitle(carContext.getString(R.string.auto_chargers_closeby))
+ .setImage(
+ CarIcon.builder(
+ IconCompat.createWithResource(
+ carContext,
+ R.drawable.ic_address
+ )
+ )
+ .setTint(CarColor.DEFAULT).build()
+ )
+ .setIsBrowsable(true)
+ .setOnClickListener {
+ screenManager.push(MapScreen(carContext, cas, favorites = false))
+ }
+ .build())
+ addItem(Row.builder()
+ .setTitle(carContext.getString(R.string.auto_favorites))
+ .setImage(
+ CarIcon.builder(
+ IconCompat.createWithResource(
+ carContext,
+ R.drawable.ic_fav
+ )
+ )
+ .setTint(CarColor.DEFAULT).build()
+ )
+ .setIsBrowsable(true)
+ .setOnClickListener {
+ screenManager.push(MapScreen(carContext, cas, favorites = true))
+ }
+ .build())
+ }.build())
+ setCurrentLocationEnabled(true)
+ setHeaderAction(Action.APP_ICON)
+ build()
+ }.build()
+ }
+
+ override fun updateLocation(location: Location) {
+ this.location = location
+ invalidate()
+ }
+}
+
+/**
+ * Screen to grant location permission
+ */
+class PermissionScreen(ctx: CarContext, val cas: CarAppService) : Screen(ctx) {
+ override fun getTemplate(): Template {
+ return MessageTemplate.builder(carContext.getString(R.string.auto_location_permission_needed))
+ .setTitle(carContext.getString(R.string.app_name))
+ .setHeaderAction(Action.APP_ICON)
+ .setActions(listOf(
+ Action.builder()
+ .setTitle(carContext.getString(R.string.grant_on_phone))
+ .setBackgroundColor(CarColor.PRIMARY)
+ .setOnClickListener(ParkedOnlyOnClickListener.create {
+ val intent = Intent(carContext, PermissionActivity::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(
+ PermissionActivity.EXTRA_RESULT_RECEIVER,
+ object : ResultReceiver(null) {
+ override fun onReceiveResult(
+ resultCode: Int,
+ resultData: Bundle?
+ ) {
+ if (resultData!!.getBoolean(PermissionActivity.RESULT_GRANTED)) {
+ cas.bindLocationService()
+ screenManager.push(WelcomeScreen(carContext, cas))
+ }
+ }
+ })
+ carContext.startActivity(intent)
+ CarToast.makeText(
+ carContext,
+ R.string.opened_on_phone,
+ CarToast.LENGTH_LONG
+ ).show()
+ })
+ .build(),
+ Action.builder()
+ .setTitle(carContext.getString(R.string.cancel))
+ .setOnClickListener {
+ cas.stopSelf()
+ }
+ .build(),
+
+ ))
+ .build()
+ }
+}
+
+/**
+ * Main map screen showing either nearby chargers or favorites
+ */
+class MapScreen(ctx: CarContext, val cas: CarAppService, val favorites: Boolean = false) :
+ Screen(ctx), LocationAwareScreen {
private var location: Location? = null
private var lastUpdateLocation: Location? = null
private var chargers: List? = null
@@ -119,8 +255,17 @@ class MapScreen(ctx: CarContext) : Screen(ctx) {
private val maxRows = 6
override fun getTemplate(): Template {
+ cas.mapScreen = this
return PlaceListMapTemplate.builder().apply {
- setTitle("EVMap")
+ setTitle(
+ carContext.getString(
+ if (favorites) {
+ R.string.auto_favorites
+ } else {
+ R.string.auto_chargers_closeby
+ }
+ )
+ )
location?.let {
setAnchor(Place.builder(LatLng.create(it)).build())
} ?: setIsLoading(true)
@@ -129,11 +274,19 @@ class MapScreen(ctx: CarContext) : Screen(ctx) {
chargerList.forEach { charger ->
builder.addItem(formatCharger(charger))
}
- builder.setNoItemsMessage(carContext.getString(R.string.auto_no_chargers_found))
+ builder.setNoItemsMessage(
+ carContext.getString(
+ if (favorites) {
+ R.string.auto_no_favorites_found
+ } else {
+ R.string.auto_no_chargers_found
+ }
+ )
+ )
setItemList(builder.build())
} ?: setIsLoading(true)
setCurrentLocationEnabled(true)
- setHeaderAction(Action.APP_ICON)
+ setHeaderAction(Action.BACK)
build()
}.build()
}
@@ -195,7 +348,7 @@ class MapScreen(ctx: CarContext) : Screen(ctx) {
}.build()
}
- fun updateLocation(location: Location) {
+ override fun updateLocation(location: Location) {
this.location = location
if (lastUpdateLocation == null) invalidate()
@@ -206,8 +359,23 @@ class MapScreen(ctx: CarContext) : Screen(ctx) {
lastUpdateLocation = location
// update displayed chargers
- lifecycleScope.launch {
- // load chargers
+ loadChargers(location)
+ }
+ }
+
+ private val db = AppDatabase.getInstance(carContext)
+
+ private fun loadChargers(location: Location) {
+ lifecycleScope.launch {
+ // load chargers
+ if (favorites) {
+ chargers = db.chargeLocationsDao().getAllChargeLocationsAsync().sortedBy {
+ distanceBetween(
+ location.latitude, location.longitude,
+ it.coordinates.lat, it.coordinates.lng
+ )
+ }
+ } else {
val response = api.getChargepointsRadius(
location.latitude,
location.longitude,
@@ -216,31 +384,31 @@ class MapScreen(ctx: CarContext) : Screen(ctx) {
)
chargers =
response.body()?.chargelocations?.filterIsInstance(ChargeLocation::class.java)
+ }
- // remove outdated availabilities
- availabilities = availabilities.filter {
- Duration.between(
- it.value.first,
- ZonedDateTime.now()
- ) > availabilityUpdateThreshold
- }.toMutableMap()
+ // remove outdated availabilities
+ availabilities = availabilities.filter {
+ Duration.between(
+ it.value.first,
+ ZonedDateTime.now()
+ ) > availabilityUpdateThreshold
+ }.toMutableMap()
- // update availabilities
- chargers?.take(maxRows)?.map {
- lifecycleScope.async {
- // update only if not yet stored
- if (!availabilities.containsKey(it.id)) {
- val date = ZonedDateTime.now()
- val availability = getAvailability(it).data
- if (availability != null) {
- availabilities[it.id] = date to availability
- }
+ // update availabilities
+ chargers?.take(maxRows)?.map {
+ lifecycleScope.async {
+ // update only if not yet stored
+ if (!availabilities.containsKey(it.id)) {
+ val date = ZonedDateTime.now()
+ val availability = getAvailability(it).data
+ if (availability != null) {
+ availabilities[it.id] = date to availability
}
}
- }?.awaitAll()
+ }
+ }?.awaitAll()
- invalidate()
- }
+ invalidate()
}
}
}
@@ -340,7 +508,6 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
R.string.opened_on_phone,
CarToast.LENGTH_LONG
).show()
- // TODO: pass options to open this specific charger
})
.build()
)
diff --git a/app/src/google/java/net/vonforst/evmap/auto/PermissionActivity.kt b/app/src/google/java/net/vonforst/evmap/auto/PermissionActivity.kt
new file mode 100644
index 00000000..13a5a468
--- /dev/null
+++ b/app/src/google/java/net/vonforst/evmap/auto/PermissionActivity.kt
@@ -0,0 +1,72 @@
+package net.vonforst.evmap.auto
+
+import android.Manifest
+import android.app.Activity
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.os.ResultReceiver
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+
+
+class PermissionActivity : Activity() {
+ companion object {
+ const val EXTRA_RESULT_RECEIVER = "result_receiver";
+ const val RESULT_GRANTED = "granted"
+ }
+
+ private lateinit var resultReceiver: ResultReceiver
+ private val permissions = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
+ private val requestCode = 1
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (intent != null) {
+ resultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER)!!
+ if (!hasPermissions(permissions)) {
+ ActivityCompat.requestPermissions(this, permissions, requestCode)
+ } else {
+ onComplete(
+ requestCode,
+ permissions,
+ intArrayOf(PackageManager.PERMISSION_GRANTED)
+ )
+ }
+ } else {
+ finish()
+ }
+ }
+
+ private fun onComplete(requestCode: Int, permissions: Array?, grantResults: IntArray) {
+ val bundle = Bundle()
+ bundle.putBoolean(
+ RESULT_GRANTED,
+ grantResults.all { it == PackageManager.PERMISSION_GRANTED })
+ resultReceiver.send(requestCode, bundle)
+ finish()
+ }
+
+ private fun hasPermissions(permissions: Array): Boolean {
+ var result = true
+ for (permission in permissions) {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ permission
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ result = false
+ break
+ }
+ }
+ return result
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ onComplete(requestCode, permissions, grantResults)
+ }
+}
\ No newline at end of file
diff --git a/app/src/google/res/values-de/values.xml b/app/src/google/res/values-de/values.xml
index 8f4e4cd1..f9ac7f37 100644
--- a/app/src/google/res/values-de/values.xml
+++ b/app/src/google/res/values-de/values.xml
@@ -7,6 +7,11 @@
Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 30% Gebühren ab.
EVMap läuft unter Android Auto und nutzt dafür deinen Standort.
Keine Ladestationen in der Nähe gefunden
+ Keine Favoriten gefunden
In App öffnen
Auf dem Telefon geöffnet
+ Um EVMap auf Android Auto zu nutzen, braucht die App Zugriff auf deinen Standort.
+ Auf Telefon zulassen
+ In der Nähe
+ Favoriten
\ No newline at end of file
diff --git a/app/src/google/res/values/values.xml b/app/src/google/res/values/values.xml
index 9b236356..02695d37 100644
--- a/app/src/google/res/values/values.xml
+++ b/app/src/google/res/values/values.xml
@@ -12,6 +12,11 @@
Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 30% off every donation.
EVMap is running on Android Auto and using your location.
No nearby chargers found
+ No favorites found
Open in app
Opened on phone
+ To run EVMap on Android Auto, you need to grant access to your location.
+ Grant on phone
+ Nearby chargers
+ Favorites
\ No newline at end of file
diff --git a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt
index 0e2a080b..24c82a6d 100644
--- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt
+++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt
@@ -14,4 +14,7 @@ interface ChargeLocationsDao {
@Query("SELECT * FROM chargelocation")
fun getAllChargeLocations(): LiveData>
+
+ @Query("SELECT * FROM chargelocation")
+ suspend fun getAllChargeLocationsAsync(): List
}
\ No newline at end of file