mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-25 16:17:45 -05:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6998382b1 | ||
|
|
5fc343d973 | ||
|
|
6b0a8bb506 | ||
|
|
93f379f4e2 | ||
|
|
00e555594a | ||
|
|
4ec5c8fb2e | ||
|
|
40b7ad8ef9 | ||
|
|
e1fed1ba26 | ||
|
|
d429ef88b3 | ||
|
|
9f0c5caf31 | ||
|
|
34b51a0742 | ||
|
|
a533fd315e | ||
|
|
d39d51d32c | ||
|
|
db11170967 | ||
|
|
4135740d07 | ||
|
|
b67bd12784 | ||
|
|
b0e000e936 | ||
|
|
1d8a7347c9 | ||
|
|
90f6cb65a8 | ||
|
|
5c57a5318b | ||
|
|
9456a6e8ef | ||
|
|
4846699f66 | ||
|
|
682f05b98b | ||
|
|
1f36ef6af8 | ||
|
|
032be00bcd | ||
|
|
3ac7b4aaee | ||
|
|
3386024acb | ||
|
|
ad2fb3063c | ||
|
|
caee3b1d67 | ||
|
|
60b151c690 | ||
|
|
e8873fa98c | ||
|
|
63740a8fe5 | ||
|
|
c80452a1fd | ||
|
|
7420101153 | ||
|
|
080d3d1080 | ||
|
|
d5ea8cfffa | ||
|
|
0676dcf31b |
@@ -8,9 +8,10 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'androidx.navigation.safeargs.kotlin'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
apply plugin: 'de.timfreiheit.resourceplaceholders'
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
@@ -18,8 +19,8 @@ android {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 31
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode 82
|
||||
versionName "1.3.3"
|
||||
versionCode 92
|
||||
versionName "1.3.5"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -94,6 +95,14 @@ android {
|
||||
disable 'NullSafeMutableLiveData'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.includeAndroidResources true
|
||||
}
|
||||
|
||||
resourcePlaceholders {
|
||||
files = ['xml/shortcuts.xml']
|
||||
}
|
||||
|
||||
// add API keys from environment variable if not set in apikeys.xml
|
||||
applicationVariants.all { variant ->
|
||||
ext.env = System.getenv()
|
||||
@@ -139,9 +148,9 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||
implementation 'androidx.core:core-ktx:1.8.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0-rc01'
|
||||
implementation "androidx.activity:activity-ktx:1.4.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.4.1"
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||
implementation "androidx.activity:activity-ktx:1.5.1"
|
||||
implementation "androidx.fragment:fragment-ktx:1.5.1"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.6.1'
|
||||
@@ -170,22 +179,24 @@ dependencies {
|
||||
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
|
||||
|
||||
// Android Auto
|
||||
googleImplementation 'androidx.car.app:app:1.2.0-rc01'
|
||||
googleNormalImplementation 'androidx.car.app:app-projected:1.2.0-rc01'
|
||||
googleAutomotiveImplementation 'androidx.car.app:app-automotive:1.2.0-rc01'
|
||||
def carAppVersion = '1.3.0-alpha01'
|
||||
googleImplementation "androidx.car.app:app:$carAppVersion"
|
||||
googleNormalImplementation "androidx.car.app:app-projected:$carAppVersion"
|
||||
googleAutomotiveImplementation "androidx.car.app:app-automotive:$carAppVersion"
|
||||
|
||||
// AnyMaps
|
||||
def anyMapsVersion = '3c67d7a1dc'
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
|
||||
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
|
||||
googleImplementation 'com.google.android.gms:play-services-maps:18.0.2'
|
||||
googleImplementation 'com.google.android.gms:play-services-maps:18.1.0'
|
||||
implementation("com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion") {
|
||||
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
|
||||
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
|
||||
exclude group: 'com.google.android.gms', module: 'play-services-location'
|
||||
}
|
||||
|
||||
// Google Places
|
||||
implementation 'com.google.android.libraries.places:places:2.6.0'
|
||||
googleImplementation 'com.google.android.libraries.places:places:2.6.0'
|
||||
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.1'
|
||||
|
||||
// Mapbox Geocoding
|
||||
@@ -196,12 +207,12 @@ dependencies {
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
// viewmodel library
|
||||
def lifecycle_version = "2.4.1"
|
||||
def lifecycle_version = "2.5.1"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
|
||||
// room library
|
||||
def room_version = "2.4.2"
|
||||
def room_version = "2.4.3"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
@@ -220,10 +231,18 @@ dependencies {
|
||||
// debug tools
|
||||
implementation 'com.facebook.stetho:stetho:1.5.1'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
|
||||
|
||||
// testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
|
||||
//noinspection GradleDependency
|
||||
testImplementation 'org.json:json:20080701'
|
||||
|
||||
// testing for car app
|
||||
testGoogleImplementation "androidx.car.app:app-testing:$carAppVersion"
|
||||
testGoogleImplementation 'org.robolectric:robolectric:4.7.3'
|
||||
testGoogleImplementation 'androidx.test:core:1.4.0'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import android.util.Log
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.ScreenManager
|
||||
import androidx.car.app.Session
|
||||
import androidx.car.app.annotations.ExperimentalCarApi
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
@@ -112,7 +113,22 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
||||
}
|
||||
|
||||
override fun onCreateScreen(intent: Intent): Screen {
|
||||
return MapScreen(carContext, this)
|
||||
val mapScreen = MapScreen(carContext, this)
|
||||
|
||||
if (!locationPermissionGranted()) {
|
||||
val screenManager = carContext.getCarService(ScreenManager::class.java)
|
||||
screenManager.push(mapScreen)
|
||||
return PermissionScreen(
|
||||
carContext,
|
||||
R.string.auto_location_permission_needed,
|
||||
listOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return mapScreen
|
||||
}
|
||||
|
||||
fun locationPermissionGranted() = carContext.checkAnyLocationPermission()
|
||||
|
||||
140
app/src/google/java/net/vonforst/evmap/auto/CarSensors.kt
Normal file
140
app/src/google/java/net/vonforst/evmap/auto/CarSensors.kt
Normal file
@@ -0,0 +1,140 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.common.CarValue
|
||||
import androidx.car.app.hardware.common.OnCarDataAvailableListener
|
||||
import androidx.car.app.hardware.info.*
|
||||
import androidx.car.app.hardware.info.CarSensors.UpdateRate
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* CarSensors is not yet implemented for Android Automotive OS
|
||||
* (see docs at https://developer.android.com/reference/androidx/car/app/hardware/info/CarSensors)
|
||||
* so we provide our own implementation based on SensorManager APIs.
|
||||
*/
|
||||
val CarContext.patchedCarSensors: CarSensors
|
||||
get() = if (BuildConfig.FLAVOR_automotive != "automotive") {
|
||||
(this.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carSensors
|
||||
} else {
|
||||
CarSensorsWrapper(this)
|
||||
}
|
||||
|
||||
class CarSensorsWrapper(carContext: CarContext) :
|
||||
CarSensors {
|
||||
private val sensorManager = carContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
private val compassListeners: MutableMap<OnCarDataAvailableListener<Compass>, SensorEventListener> =
|
||||
mutableMapOf()
|
||||
|
||||
override fun addAccelerometerListener(
|
||||
rate: Int,
|
||||
executor: Executor,
|
||||
listener: OnCarDataAvailableListener<Accelerometer>
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun removeAccelerometerListener(listener: OnCarDataAvailableListener<Accelerometer>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addGyroscopeListener(
|
||||
rate: Int,
|
||||
executor: Executor,
|
||||
listener: OnCarDataAvailableListener<Gyroscope>
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun removeGyroscopeListener(listener: OnCarDataAvailableListener<Gyroscope>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addCompassListener(
|
||||
rate: Int,
|
||||
executor: Executor,
|
||||
listener: OnCarDataAvailableListener<Compass>
|
||||
) {
|
||||
val magSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
|
||||
val accSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||
|
||||
if (magSensor == null) {
|
||||
executor.execute {
|
||||
listener.onCarDataAvailable(Compass(CarValue(null, 0, CarValue.STATUS_UNAVAILABLE)))
|
||||
}
|
||||
return
|
||||
}
|
||||
val sensorListener = object : SensorEventListener {
|
||||
var magValues: FloatArray? = null
|
||||
|
||||
// AAOS cars may not provide an acceleration sensor, so we assume acceleration based on
|
||||
// Earth's gravity. May not be correct when driving on other planets.
|
||||
var accValues = floatArrayOf(0f, 0f, SensorManager.GRAVITY_EARTH)
|
||||
val rotMatrix = FloatArray(9)
|
||||
val orientation = FloatArray(3)
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
when (event.sensor) {
|
||||
magSensor -> magValues = event.values
|
||||
accSensor -> accValues = event.values
|
||||
}
|
||||
if (magValues == null) return
|
||||
|
||||
SensorManager.getRotationMatrix(rotMatrix, null, accValues, magValues)
|
||||
SensorManager.getOrientation(rotMatrix, orientation)
|
||||
val compassDegrees = orientation.map { Math.toDegrees(it.toDouble()).toFloat() }
|
||||
executor.execute {
|
||||
listener.onCarDataAvailable(
|
||||
Compass(
|
||||
CarValue(
|
||||
compassDegrees,
|
||||
event.timestamp,
|
||||
CarValue.STATUS_SUCCESS
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
|
||||
}
|
||||
|
||||
}
|
||||
compassListeners[listener] = sensorListener
|
||||
sensorManager.registerListener(sensorListener, magSensor, mapRate(rate))
|
||||
accSensor?.let { sensorManager.registerListener(sensorListener, it, mapRate(rate)) }
|
||||
}
|
||||
|
||||
private fun mapRate(@UpdateRate rate: Int): Int {
|
||||
return when (rate) {
|
||||
CarSensors.UPDATE_RATE_NORMAL -> SensorManager.SENSOR_DELAY_NORMAL
|
||||
CarSensors.UPDATE_RATE_UI -> SensorManager.SENSOR_DELAY_UI
|
||||
CarSensors.UPDATE_RATE_FASTEST -> SensorManager.SENSOR_DELAY_FASTEST
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeCompassListener(listener: OnCarDataAvailableListener<Compass>) {
|
||||
compassListeners[listener]?.let {
|
||||
sensorManager.unregisterListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addCarHardwareLocationListener(
|
||||
rate: Int,
|
||||
executor: Executor,
|
||||
listener: OnCarDataAvailableListener<CarHardwareLocation>
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun removeCarHardwareLocationListener(listener: OnCarDataAvailableListener<CarHardwareLocation>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
|
||||
|
||||
private val imageSize = 128 // images should be 128dp according to docs
|
||||
private val imageHeightLarge = 480 // images should be 480 x 854 dp according to docs
|
||||
private val imageWidthLarge = 854
|
||||
private val imageSizeLarge = 480 // images should be 480 x 480 dp according to docs
|
||||
|
||||
private val iconGen =
|
||||
ChargerIconGenerator(carContext, null, height = imageSize)
|
||||
@@ -369,10 +368,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
photo?.let {
|
||||
val density = carContext.resources.displayMetrics.density
|
||||
val url = if (largeImageSupported) {
|
||||
photo.getUrl(
|
||||
width = (imageWidthLarge * density).roundToInt(),
|
||||
height = (imageHeightLarge * density).roundToInt()
|
||||
)
|
||||
photo.getUrl(size = (imageSizeLarge * density).roundToInt())
|
||||
} else {
|
||||
photo.getUrl(size = (imageSize * density).roundToInt())
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import net.vonforst.evmap.viewmodel.FilterViewModel
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class FilterScreen(ctx: CarContext) : Screen(ctx) {
|
||||
class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(ctx)
|
||||
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
|
||||
@@ -65,42 +65,6 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
|
||||
}
|
||||
})
|
||||
}.build())
|
||||
if (filterStatus !in listOf(
|
||||
FILTERS_CUSTOM,
|
||||
FILTERS_FAVORITES,
|
||||
FILTERS_DISABLED
|
||||
)
|
||||
) {
|
||||
addAction(Action.Builder().apply {
|
||||
setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_delete
|
||||
)
|
||||
).build()
|
||||
|
||||
)
|
||||
setOnClickListener {
|
||||
val currentProfile =
|
||||
filterProfiles.value?.find { it.id == filterStatus }
|
||||
?: return@setOnClickListener
|
||||
lifecycleScope.launch {
|
||||
db.filterProfileDao().delete(currentProfile)
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
carContext.getString(
|
||||
R.string.deleted_filterprofile,
|
||||
currentProfile.name
|
||||
),
|
||||
CarToast.LENGTH_SHORT
|
||||
).show()
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
}.build()
|
||||
)
|
||||
}.build()
|
||||
@@ -190,33 +154,46 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
|
||||
setHeaderAction(Action.BACK)
|
||||
setActionStrip(ActionStrip.Builder().apply {
|
||||
addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_check
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
vm.saveFilterValues()
|
||||
screenManager.popTo(MapScreen.MARKER)
|
||||
val currentProfile = vm.filterProfile.value
|
||||
if (currentProfile != null) {
|
||||
addAction(Action.Builder().apply {
|
||||
setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_delete
|
||||
)
|
||||
).build()
|
||||
|
||||
)
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
vm.deleteCurrentProfile()
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
carContext.getString(
|
||||
R.string.deleted_filterprofile,
|
||||
currentProfile.name
|
||||
),
|
||||
CarToast.LENGTH_SHORT
|
||||
).show()
|
||||
invalidate()
|
||||
screenManager.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
)
|
||||
addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_save
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
}.build())
|
||||
}
|
||||
addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_save
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
val textPromptScreen = TextPromptScreen(
|
||||
carContext,
|
||||
R.string.save_as_profile,
|
||||
@@ -248,18 +225,21 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
is BooleanFilter -> {
|
||||
setToggle(Toggle.Builder {
|
||||
(value as BooleanFilterValue).value = it
|
||||
lifecycleScope.launch { vm.saveFilterValues() }
|
||||
}.setChecked((value as BooleanFilterValue).value).build())
|
||||
}
|
||||
is MultipleChoiceFilter -> {
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
screenManager.push(
|
||||
screenManager.pushForResult(
|
||||
MultipleChoiceFilterScreen(
|
||||
carContext,
|
||||
filter,
|
||||
value as MultipleChoiceFilterValue
|
||||
)
|
||||
)
|
||||
) {
|
||||
lifecycleScope.launch { vm.saveFilterValues() }
|
||||
}
|
||||
}
|
||||
addText(
|
||||
if ((value as MultipleChoiceFilterValue).all) {
|
||||
@@ -276,13 +256,15 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
setBrowsable(true)
|
||||
addText((value as SliderFilterValue).value.toString() + " " + filter.unit)
|
||||
setOnClickListener {
|
||||
screenManager.push(
|
||||
screenManager.pushForResult(
|
||||
SliderFilterScreen(
|
||||
carContext,
|
||||
filter,
|
||||
value
|
||||
)
|
||||
)
|
||||
) {
|
||||
lifecycleScope.launch { vm.saveFilterValues() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -379,7 +361,7 @@ class SliderFilterScreen(
|
||||
private fun generateSlider(): CharSequence {
|
||||
val bar = "━"
|
||||
val dot = "⬤"
|
||||
val length = 35
|
||||
val length = 30
|
||||
|
||||
val position =
|
||||
((filter.inverseMapping(value.value) - filter.min) / (filter.max - filter.min).toDouble() * length).roundToInt()
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.car.app.CarContext
|
||||
@@ -40,6 +37,7 @@ import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
@@ -48,7 +46,7 @@ import kotlin.math.roundToInt
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
Screen(ctx), LocationAwareScreen, OnContentRefreshListener,
|
||||
ItemList.OnItemVisibilityChangedListener {
|
||||
ItemList.OnItemVisibilityChangedListener, DefaultLifecycleObserver {
|
||||
companion object {
|
||||
val MARKER = "map"
|
||||
}
|
||||
@@ -85,7 +83,9 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
Transformations.map(referenceData) { api.getFilters(it, carContext.stringProvider()) }
|
||||
private val filtersWithValue = filtersWithValue(filters, filterValues)
|
||||
|
||||
private val hardwareMan = ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
private val hardwareMan: CarHardwareManager by lazy {
|
||||
ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
}
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
private val permissions = if (BuildConfig.FLAVOR_automotive == "automotive") {
|
||||
listOf(
|
||||
@@ -99,6 +99,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
)
|
||||
}
|
||||
|
||||
private var searchLocation: LatLng? = null
|
||||
|
||||
init {
|
||||
filtersWithValue.observe(this) {
|
||||
loadChargers()
|
||||
@@ -107,12 +109,14 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
checkLocationPermission()
|
||||
session.requestLocationUpdates()
|
||||
|
||||
session.mapScreen = this
|
||||
return PlaceListMapTemplate.Builder().apply {
|
||||
setTitle(
|
||||
carContext.getString(
|
||||
prefs.placeSearchResultAndroidAutoName?.let {
|
||||
carContext.getString(R.string.auto_chargers_near_location, it)
|
||||
} ?: carContext.getString(
|
||||
if (filterStatus.value == FILTERS_FAVORITES) {
|
||||
R.string.auto_favorites
|
||||
} else {
|
||||
@@ -120,8 +124,16 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
)
|
||||
)
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it)).build())
|
||||
searchLocation?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
|
||||
if (prefs.placeSearchResultAndroidAutoName != null) {
|
||||
setMarker(
|
||||
PlaceMarker.Builder()
|
||||
.setColor(CarColor.PRIMARY)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
} ?: setLoading(true)
|
||||
chargers?.take(maxRows)?.let { chargerList ->
|
||||
val builder = ItemList.Builder()
|
||||
@@ -166,6 +178,42 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
session.mapScreen = null
|
||||
}
|
||||
.build())
|
||||
.addAction(Action.Builder().apply {
|
||||
setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
||||
R.drawable.ic_search_off
|
||||
} else {
|
||||
R.drawable.ic_search
|
||||
}
|
||||
)
|
||||
).build()
|
||||
|
||||
)
|
||||
setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
||||
prefs.placeSearchResultAndroidAutoName = null
|
||||
prefs.placeSearchResultAndroidAuto = null
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
} else {
|
||||
screenManager.pushForResult(
|
||||
PlaceSearchScreen(
|
||||
carContext,
|
||||
session
|
||||
)
|
||||
) {
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
session.mapScreen = null
|
||||
}
|
||||
})
|
||||
}.build())
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
@@ -179,7 +227,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
.build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
screenManager.pushForResult(FilterScreen(carContext)) {
|
||||
screenManager.pushForResult(FilterScreen(carContext, session)) {
|
||||
chargers = null
|
||||
filterStatus.value = prefs.filterStatus
|
||||
}
|
||||
@@ -191,25 +239,6 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun checkLocationPermission() {
|
||||
if (!session.locationPermissionGranted()) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
screenManager.pushForResult(
|
||||
PermissionScreen(
|
||||
carContext,
|
||||
R.string.auto_location_permission_needed,
|
||||
listOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
)
|
||||
) {
|
||||
session.requestLocationUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatCharger(charger: ChargeLocation, showCity: Boolean): Row {
|
||||
val markerTint = if ((charger.maxPower ?: 0.0) > 100) {
|
||||
R.color.charger_100kw_dark // slightly darker color for better contrast
|
||||
@@ -325,6 +354,10 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
val referenceData = referenceData.value ?: return
|
||||
val filters = filtersWithValue.value ?: return
|
||||
|
||||
val searchLocation =
|
||||
prefs.placeSearchResultAndroidAuto ?: LatLng.fromLocation(location)
|
||||
this.searchLocation = searchLocation
|
||||
|
||||
updateCoroutine = lifecycleScope.launch {
|
||||
try {
|
||||
// load chargers
|
||||
@@ -339,7 +372,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
} else {
|
||||
val response = api.getChargepointsRadius(
|
||||
referenceData,
|
||||
LatLng.fromLocation(location),
|
||||
searchLocation,
|
||||
searchRadius,
|
||||
zoom = 16f,
|
||||
filters
|
||||
@@ -350,7 +383,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
// try again with larger radius
|
||||
val response = api.getChargepointsRadius(
|
||||
referenceData,
|
||||
LatLng.fromLocation(location),
|
||||
searchLocation,
|
||||
searchRadius * 10,
|
||||
zoom = 16f,
|
||||
filters
|
||||
@@ -374,11 +407,15 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
|
||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
||||
val isUpdate = this.energyLevel == null
|
||||
this.energyLevel = energyLevel
|
||||
invalidate()
|
||||
if (isUpdate) invalidate()
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
setupListeners()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
private fun setupListeners() {
|
||||
if (!permissions.all {
|
||||
ContextCompat.checkSelfPermission(
|
||||
@@ -388,25 +425,39 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
})
|
||||
return
|
||||
|
||||
println("Setting up energy level listener")
|
||||
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
println("Setting up energy level listener")
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
removeListeners()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
private fun removeListeners() {
|
||||
println("Removing energy level listener")
|
||||
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
println("Removing energy level listener")
|
||||
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContentRefreshRequested() {
|
||||
loadChargers()
|
||||
availabilities.clear()
|
||||
|
||||
val start = visibleStart
|
||||
val end = visibleEnd
|
||||
if (start != null && end != null) {
|
||||
onItemVisibilityChanged(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemVisibilityChanged(startIndex: Int, endIndex: Int) {
|
||||
// when the list is scrolled, load corresponding availabilities
|
||||
if (startIndex == visibleStart && endIndex == visibleEnd) return
|
||||
if (startIndex == visibleStart && endIndex == visibleEnd && !availabilities.isEmpty()) return
|
||||
if (startIndex == -1 || endIndex == -1) return
|
||||
if (availabilityUpdateCoroutine != null) return
|
||||
|
||||
visibleEnd = endIndex
|
||||
@@ -423,7 +474,14 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
// update availabilities
|
||||
availabilityUpdateCoroutine = lifecycleScope.launch {
|
||||
delay(300L)
|
||||
val tasks = chargers?.subList(startIndex, endIndex)?.mapNotNull {
|
||||
|
||||
val chargers = chargers ?: return@launch
|
||||
if (chargers.isEmpty()) return@launch
|
||||
|
||||
val tasks = chargers.subList(
|
||||
min(startIndex, chargers.size - 1),
|
||||
min(endIndex, chargers.size - 1)
|
||||
).mapNotNull {
|
||||
// update only if not yet stored
|
||||
if (!availabilities.containsKey(it.id)) {
|
||||
lifecycleScope.async {
|
||||
@@ -433,7 +491,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
} else null
|
||||
}
|
||||
if (!tasks.isNullOrEmpty()) {
|
||||
if (tasks.isNotEmpty()) {
|
||||
tasks.awaitAll()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
224
app/src/google/java/net/vonforst/evmap/auto/PlaceSearchScreen.kt
Normal file
224
app/src/google/java/net/vonforst/evmap/auto/PlaceSearchScreen.kt
Normal file
@@ -0,0 +1,224 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.annotations.ExperimentalCarApi
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.info.EnergyLevel
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.car2go.maps.model.LatLng
|
||||
import kotlinx.coroutines.*
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.iconForPlaceType
|
||||
import net.vonforst.evmap.adapter.isSpecialPlace
|
||||
import net.vonforst.evmap.autocomplete.*
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.storage.RecentAutocompletePlace
|
||||
import java.time.Instant
|
||||
|
||||
@ExperimentalCarApi
|
||||
class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx),
|
||||
SearchTemplate.SearchCallback, LocationAwareScreen,
|
||||
DefaultLifecycleObserver {
|
||||
private val hardwareMan: CarHardwareManager by lazy {
|
||||
ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
}
|
||||
private var resultList: List<AutocompletePlace>? = null
|
||||
private var recentResults = mutableListOf<RecentAutocompletePlace>()
|
||||
private var currentProvider: AutocompleteProvider? = null
|
||||
private val providers = getAutocompleteProviders(ctx)
|
||||
private val recents = AppDatabase.getInstance(ctx).recentAutocompletePlaceDao()
|
||||
private val maxItems = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
private var location: Location? = null
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
private var updateJob: Job? = null
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
|
||||
private val permissions = if (BuildConfig.FLAVOR_automotive == "automotive") {
|
||||
listOf(
|
||||
"android.car.permission.CAR_ENERGY",
|
||||
"android.car.permission.CAR_ENERGY_PORTS",
|
||||
"android.car.permission.READ_CAR_DISPLAY_UNITS",
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
"com.google.android.gms.permission.CAR_FUEL"
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
update("")
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return SearchTemplate.Builder(this).apply {
|
||||
setHeaderAction(Action.BACK)
|
||||
setSearchHint(carContext.getString(R.string.search))
|
||||
resultList?.let {
|
||||
setItemList(buildItemList(it))
|
||||
} ?: setLoading(true)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun buildItemList(results: List<AutocompletePlace>): ItemList {
|
||||
return ItemList.Builder().apply {
|
||||
results.forEach { place ->
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(place.primaryText)
|
||||
addText(place.secondaryText)
|
||||
|
||||
val icon = iconForPlaceType(place.types)
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithResource(carContext, icon))
|
||||
.setTint(if (isSpecialPlace(place.types)) CarColor.PRIMARY else CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
|
||||
// distance
|
||||
place.distanceMeters?.let {
|
||||
val text = SpannableStringBuilder()
|
||||
text.append(
|
||||
"distance",
|
||||
DistanceSpan.create(
|
||||
roundValueToDistance(
|
||||
it,
|
||||
energyLevel?.distanceDisplayUnit?.value
|
||||
)
|
||||
),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
addText(text)
|
||||
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
val placeDetails = getDetails(place.id)
|
||||
prefs.placeSearchResultAndroidAuto = placeDetails.latLng
|
||||
prefs.placeSearchResultAndroidAutoName =
|
||||
place.primaryText.toString()
|
||||
screenManager.popTo(MapScreen.MARKER)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun onSearchTextChanged(searchText: String) {
|
||||
update(searchText)
|
||||
}
|
||||
|
||||
override fun onSearchSubmitted(searchText: String) {
|
||||
update(searchText)
|
||||
}
|
||||
|
||||
private fun update(searchText: String) {
|
||||
updateJob?.cancel()
|
||||
updateJob = lifecycleScope.launch {
|
||||
if (prefs.searchProvider == "mapbox" && !isShortQuery(searchText)) {
|
||||
delay(500L)
|
||||
}
|
||||
loadNewList(searchText)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadNewList(query: String) {
|
||||
for (provider in providers) {
|
||||
try {
|
||||
recentResults.clear()
|
||||
currentProvider = provider
|
||||
|
||||
// first search in recent places
|
||||
val recentPlaces = if (query.isEmpty()) {
|
||||
recents.getAllAsync(provider.id, limit = maxItems)
|
||||
} else {
|
||||
recents.searchAsync(query, provider.id, limit = maxItems)
|
||||
}
|
||||
recentResults.addAll(recentPlaces)
|
||||
resultList =
|
||||
recentPlaces.map { it.asAutocompletePlace(LatLng.fromLocation(location)) }
|
||||
invalidate()
|
||||
|
||||
// if we already have enough results or the query is short, stop here
|
||||
if (isShortQuery(query) || recentResults.size >= maxItems) break
|
||||
|
||||
// then search online
|
||||
val recentIds = recentPlaces.map { it.id }
|
||||
resultList = withContext(Dispatchers.IO) {
|
||||
(resultList!! + provider.autocomplete(query, LatLng.fromLocation(location))
|
||||
.filter { !recentIds.contains(it.id) }).take(maxItems)
|
||||
}
|
||||
invalidate()
|
||||
break
|
||||
} catch (e: ApiUnavailableException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isShortQuery(query: CharSequence) = query.length < 3
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
this.location = location
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
session.requestLocationUpdates()
|
||||
session.mapScreen = this
|
||||
|
||||
if (supportsCarApiLevel3(carContext) && permissions.all {
|
||||
ContextCompat.checkSelfPermission(
|
||||
carContext,
|
||||
it
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}) {
|
||||
|
||||
println("Setting up energy level listener")
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
||||
val isUpdate = this.energyLevel == null
|
||||
this.energyLevel = energyLevel
|
||||
if (isUpdate) invalidate()
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
session.mapScreen = null
|
||||
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getDetails(id: String): PlaceWithBounds {
|
||||
val provider = currentProvider!!
|
||||
val result = resultList!!.find { it.id == id }!!
|
||||
|
||||
val recentPlace = recentResults.find { it.id == id }
|
||||
if (recentPlace != null) return recentPlace.asPlaceWithBounds()
|
||||
|
||||
val details = provider.getDetails(id)
|
||||
|
||||
recents.insert(RecentAutocompletePlace(result, details, provider.id, Instant.now()))
|
||||
|
||||
return details
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,30 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
val dataSourceNames = carContext.resources.getStringArray(R.array.pref_data_source_names)
|
||||
val dataSourceValues = carContext.resources.getStringArray(R.array.pref_data_source_values)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.auto_settings))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_data_source))
|
||||
val dataSourceId = prefs.dataSource
|
||||
val dataSourceDesc = dataSourceNames[dataSourceValues.indexOf(dataSourceId)]
|
||||
addText(dataSourceDesc)
|
||||
setTitle(carContext.getString(R.string.settings_data_sources))
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
@@ -40,7 +37,7 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
)
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
screenManager.push(ChooseDataSourceScreen(carContext))
|
||||
screenManager.push(DataSettingsScreen(carContext))
|
||||
}
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
@@ -81,31 +78,107 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
class ChooseDataSourceScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
val db = AppDatabase.getInstance(ctx)
|
||||
val dataSourceNames = carContext.resources.getStringArray(R.array.pref_data_source_names)
|
||||
val dataSourceValues = carContext.resources.getStringArray(R.array.pref_data_source_values)
|
||||
val dataSourceDescriptions = listOf(
|
||||
carContext.getString(R.string.data_source_goingelectric_desc),
|
||||
carContext.getString(R.string.data_source_openchargemap_desc)
|
||||
)
|
||||
val searchProviderNames =
|
||||
carContext.resources.getStringArray(R.array.pref_search_provider_names)
|
||||
val searchProviderValues =
|
||||
carContext.resources.getStringArray(R.array.pref_search_provider_values)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.settings_data_sources))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_data_source))
|
||||
setBrowsable(true)
|
||||
val dataSourceId = prefs.dataSource
|
||||
val dataSourceDesc = dataSourceNames[dataSourceValues.indexOf(dataSourceId)]
|
||||
addText(dataSourceDesc)
|
||||
setOnClickListener {
|
||||
screenManager.push(
|
||||
ChooseDataSourceScreen(
|
||||
carContext,
|
||||
dataSourceNames,
|
||||
dataSourceValues,
|
||||
prefs.dataSource,
|
||||
dataSourceDescriptions
|
||||
) {
|
||||
prefs.dataSource = it
|
||||
})
|
||||
}
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_search_provider))
|
||||
setBrowsable(true)
|
||||
val searchProviderId = prefs.searchProvider
|
||||
val searchProviderDesc =
|
||||
searchProviderNames[searchProviderValues.indexOf(searchProviderId)]
|
||||
addText(searchProviderDesc)
|
||||
setOnClickListener {
|
||||
screenManager.push(
|
||||
ChooseDataSourceScreen(
|
||||
carContext,
|
||||
searchProviderNames,
|
||||
searchProviderValues,
|
||||
prefs.searchProvider
|
||||
) {
|
||||
prefs.searchProvider = it
|
||||
})
|
||||
}
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_search_delete_recent))
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
db.recentAutocompletePlaceDao().deleteAll()
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.deleted_recent_search_results,
|
||||
CarToast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
class ChooseDataSourceScreen(
|
||||
ctx: CarContext,
|
||||
val names: Array<String>,
|
||||
val values: Array<String>,
|
||||
val currentValue: String,
|
||||
val descriptions: List<String>? = null,
|
||||
val callback: (String) -> Unit
|
||||
) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_data_source))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
for (i in dataSourceNames.indices) {
|
||||
for (i in names.indices) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(dataSourceNames[i])
|
||||
addText(dataSourceDescriptions[i])
|
||||
setTitle(names[i])
|
||||
descriptions?.let { addText(it[i]) }
|
||||
}.build())
|
||||
}
|
||||
setOnSelectedListener {
|
||||
prefs.dataSource = dataSourceValues[it]
|
||||
callback(values[it])
|
||||
screenManager.pop()
|
||||
}
|
||||
setSelectedIndex(dataSourceValues.indexOf(prefs.dataSource))
|
||||
setSelectedIndex(values.indexOf(currentValue))
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
|
||||
@@ -3,31 +3,59 @@ package net.vonforst.evmap.auto
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.InputCallback
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.model.*
|
||||
import androidx.car.app.model.signin.InputSignInMethod
|
||||
import androidx.car.app.model.signin.SignInTemplate
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
class TextPromptScreen(
|
||||
ctx: CarContext,
|
||||
@StringRes val title: Int,
|
||||
@StringRes val prompt: Int,
|
||||
val initialValue: String? = null
|
||||
val initialValue: String? = null,
|
||||
val cancelable: Boolean = true
|
||||
) : Screen(ctx),
|
||||
InputCallback {
|
||||
private var inputText = ""
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val signInMethod = InputSignInMethod.Builder(this).apply {
|
||||
initialValue?.let { setDefaultValue(it) }
|
||||
initialValue?.let {
|
||||
setDefaultValue(it)
|
||||
inputText = initialValue
|
||||
}
|
||||
setShowKeyboardByDefault(true)
|
||||
}.build()
|
||||
return SignInTemplate.Builder(signInMethod).apply {
|
||||
setHeaderAction(Action.BACK)
|
||||
setInstructions(carContext.getString(prompt))
|
||||
setTitle(carContext.getString(title))
|
||||
if (cancelable) {
|
||||
addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.cancel))
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
screenManager.pop()
|
||||
})
|
||||
.build()
|
||||
)
|
||||
}
|
||||
addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.ok))
|
||||
.setBackgroundColor(CarColor.PRIMARY)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
onInputSubmitted(inputText)
|
||||
})
|
||||
.build()
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun onInputTextChanged(text: String) {
|
||||
inputText = text
|
||||
}
|
||||
|
||||
override fun onInputSubmitted(text: String) {
|
||||
setResult(text)
|
||||
screenManager.pop()
|
||||
|
||||
@@ -3,13 +3,13 @@ package net.vonforst.evmap.auto
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.hardware.common.CarUnit
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.Distance
|
||||
import androidx.car.app.model.*
|
||||
import androidx.car.app.versioning.CarAppApiLevels
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
@@ -152,4 +152,17 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
class DummyReturnScreen(ctx: CarContext) : Screen(ctx) {
|
||||
/*
|
||||
Dummy screen to get around template refresh limitations.
|
||||
It immediately pops back to the previous screen.
|
||||
*/
|
||||
override fun onGetTemplate(): Template {
|
||||
screenManager.pop()
|
||||
return MessageTemplate.Builder(carContext.getString(R.string.loading)).setLoading(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,9 +10,8 @@ import androidx.car.app.hardware.info.*
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.CompassNeedle
|
||||
@@ -20,8 +19,10 @@ import net.vonforst.evmap.ui.Gauge
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class VehicleDataScreen(ctx: CarContext) : Screen(ctx), LifecycleObserver {
|
||||
private val hardwareMan = ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver {
|
||||
private val carInfo =
|
||||
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
|
||||
private val carSensors = carContext.patchedCarSensors
|
||||
private var model: Model? = null
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
private var speed: Speed? = null
|
||||
@@ -225,33 +226,39 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), LifecycleObserver {
|
||||
invalidate()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
setupListeners()
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
if (!permissionsGranted()) return
|
||||
|
||||
println("Setting up energy level listener")
|
||||
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
hardwareMan.carInfo.addSpeedListener(exec, ::onSpeedUpdated)
|
||||
hardwareMan.carSensors.addCompassListener(
|
||||
carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
carInfo.addSpeedListener(exec, ::onSpeedUpdated)
|
||||
carSensors.addCompassListener(
|
||||
CarSensors.UPDATE_RATE_NORMAL,
|
||||
exec,
|
||||
::onCompassUpdated
|
||||
)
|
||||
|
||||
hardwareMan.carInfo.fetchModel(exec) {
|
||||
carInfo.fetchModel(exec) {
|
||||
this.model = it
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
removeListeners()
|
||||
}
|
||||
|
||||
private fun removeListeners() {
|
||||
println("Removing energy level listener")
|
||||
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
hardwareMan.carInfo.removeSpeedListener(::onSpeedUpdated)
|
||||
hardwareMan.carSensors.removeCompassListener(::onCompassUpdated)
|
||||
carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
carInfo.removeSpeedListener(::onSpeedUpdated)
|
||||
carSensors.removeCompassListener(::onCompassUpdated)
|
||||
}
|
||||
|
||||
private fun permissionsGranted(): Boolean =
|
||||
|
||||
13
app/src/google/res/drawable/ic_search_off.xml
Normal file
13
app/src/google/res/drawable/ic_search_off.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5C16,5.91 13.09,3 9.5,3C6.08,3 3.28,5.64 3.03,9h2.02C5.3,6.75 7.18,5 9.5,5C11.99,5 14,7.01 14,9.5S11.99,14 9.5,14c-0.17,0 -0.33,-0.03 -0.5,-0.05v2.02C9.17,15.99 9.33,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57L14,14.71v0.79l5,4.99L20.49,19L15.5,14z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6.47,10.82l-2.47,2.47l-2.47,-2.47l-0.71,0.71l2.47,2.47l-2.47,2.47l0.71,0.71l2.47,-2.47l2.47,2.47l0.71,-0.71l-2.47,-2.47l2.47,-2.47z" />
|
||||
</vector>
|
||||
@@ -19,6 +19,7 @@
|
||||
<string name="grant_on_phone">Auf Telefon zulassen</string>
|
||||
<string name="auto_chargers_closeby">In der Nähe</string>
|
||||
<string name="auto_favorites">Favoriten</string>
|
||||
<string name="auto_chargers_near_location">Nahe %s</string>
|
||||
<string name="auto_fault_report_date">⚠️ Störungsmeldung (%s)</string>
|
||||
<string name="auto_no_refresh_possible">Weitere Aktualisierung nicht möglich. Bitte zurück gehen und neu starten.</string>
|
||||
<string name="auto_prices">Preise</string>
|
||||
@@ -39,4 +40,5 @@
|
||||
<string name="data_sources_hint">In den Einstellungen kannst du auch zwischen Google Maps und OpenStreetMap (Mapbox) für die Kartendaten wechseln.</string>
|
||||
<string name="selecting_all">alle Einträge ausgewählt</string>
|
||||
<string name="selecting_none">alle Einträge abgewählt</string>
|
||||
<string name="loading">Lade…</string>
|
||||
</resources>
|
||||
@@ -29,6 +29,7 @@
|
||||
<string name="grant_on_phone">Grant on phone</string>
|
||||
<string name="auto_chargers_closeby">Nearby chargers</string>
|
||||
<string name="auto_favorites">Favorites</string>
|
||||
<string name="auto_chargers_near_location">Near %s</string>
|
||||
<string name="auto_fault_report_date">⚠️ Fault report (%s)</string>
|
||||
<string name="auto_no_refresh_possible">Further updates not possible. Please go back and restart.</string>
|
||||
<string name="auto_prices">Pricing</string>
|
||||
@@ -49,4 +50,5 @@
|
||||
<string name="data_sources_hint">In the settings you can also switch between Google Maps and OpenStreetMap (Mapbox) for the map data.</string>
|
||||
<string name="selecting_all">selected all items</string>
|
||||
<string name="selecting_none">deselected all items</string>
|
||||
<string name="loading">Loading…</string>
|
||||
</resources>
|
||||
@@ -84,10 +84,10 @@ class MapsActivity : AppCompatActivity(),
|
||||
val navView = findViewById<NavigationView>(R.id.nav_view)
|
||||
navView.setupWithNavController(navController)
|
||||
|
||||
val header = navView.getHeaderView(0)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(header) { v, insets ->
|
||||
v.setPadding(0, insets.getInsets(WindowInsetsCompat.Type.statusBars()).top, 0, 0)
|
||||
insets
|
||||
ViewCompat.setOnApplyWindowInsetsListener(navView) { v, insets ->
|
||||
val header = navView.getHeaderView(0)
|
||||
header.setPadding(0, insets.getInsets(WindowInsetsCompat.Type.statusBars()).top, 0, 0)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
prefs = PreferenceDataSource(this)
|
||||
@@ -163,6 +163,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
.createPendingIntent()
|
||||
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(navGraph)
|
||||
.setDestination(R.id.favs)
|
||||
.createPendingIntent()
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
|
||||
root.post {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
|
||||
getCheckedItem()?.let { onCheckedItemChangedListener?.invoke(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ data class ChargeLocationStatus(
|
||||
(connectors == null || connectors.map {
|
||||
equivalentPlugTypes(it)
|
||||
}.any { equivalent -> it.type in equivalent })
|
||||
&& (minPower == null || (it.power != null && it.power > minPower))
|
||||
&& (minPower == null || (it.power != null && it.power >= minPower))
|
||||
}
|
||||
return this.copy(status = statusFiltered)
|
||||
}
|
||||
@@ -191,4 +191,4 @@ suspend fun getAvailability(charger: ChargeLocation): Resource<ChargeLocationSta
|
||||
}
|
||||
}
|
||||
return value ?: Resource.error(null, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ package net.vonforst.evmap.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -24,6 +28,7 @@ import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
@@ -45,6 +50,28 @@ class ChargepriceFragment : Fragment() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedElementEnterTransition = MaterialContainerTransform()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val prefs = PreferenceDataSource(requireContext())
|
||||
prefs.chargepriceCounter += 1
|
||||
if ((prefs.chargepriceCounter - 30).mod(50) == 0) {
|
||||
showDonationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDonationDialog() {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.chargeprice_donation_dialog_title)
|
||||
.setMessage(R.string.chargeprice_donation_dialog_detail)
|
||||
.setNegativeButton(R.string.ok) { di, _ ->
|
||||
di.cancel()
|
||||
}
|
||||
.setPositiveButton(R.string.donate) { di, _ ->
|
||||
di.dismiss()
|
||||
findNavController().navigate(R.id.action_chargeprice_to_donateFragment)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@@ -147,7 +174,7 @@ class ChargepriceFragment : Fragment() {
|
||||
}
|
||||
|
||||
binding.btnSettings.setOnClickListener {
|
||||
findNavController().navigate(R.id.action_chargeprice_to_settingsFragment)
|
||||
findNavController().navigate(R.id.action_chargeprice_to_chargepriceSettingsFragment)
|
||||
}
|
||||
|
||||
binding.batteryRange.setLabelFormatter { value: Float ->
|
||||
|
||||
@@ -23,10 +23,7 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.MenuCompat
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.*
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
@@ -116,6 +113,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
private var searchResultIcon: BitmapDescriptor? = null
|
||||
private var connectionErrorSnackbar: Snackbar? = null
|
||||
private var previousChargepointIds: Set<Long>? = null
|
||||
private var mapTopPadding: Int = 0
|
||||
|
||||
private lateinit var clusterIconGenerator: ClusterIconGenerator
|
||||
private lateinit var chargerIconGenerator: ChargerIconGenerator
|
||||
@@ -173,7 +171,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val provider = prefs.mapProvider
|
||||
if (mapFragment == null) {
|
||||
mapFragment =
|
||||
requireActivity().supportFragmentManager.findFragmentByTag(mapFragmentTag) as MapFragment?
|
||||
childFragmentManager.findFragmentByTag(mapFragmentTag) as MapFragment?
|
||||
}
|
||||
if (mapFragment == null || mapFragment!!.priority[0] != provider) {
|
||||
mapFragment = MapFragment()
|
||||
@@ -186,7 +184,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
MapFragment.GOOGLE,
|
||||
MapFragment.MAPBOX
|
||||
)
|
||||
requireActivity().supportFragmentManager
|
||||
childFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.map, mapFragment!!, mapFragmentTag)
|
||||
.commit()
|
||||
@@ -201,19 +199,23 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.root
|
||||
) { v, insets ->
|
||||
ViewCompat.onApplyWindowInsets(binding.root, insets)
|
||||
|
||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
||||
binding.detailAppBar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.systemWindowInsetTop
|
||||
topMargin = systemWindowInsetTop
|
||||
}
|
||||
|
||||
// margin of layers button
|
||||
// margin of layers button: status bar height + toolbar height + margin
|
||||
val density = resources.displayMetrics.density
|
||||
// status bar height + toolbar height + margin
|
||||
val margin =
|
||||
if (binding.toolbarContainer.layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
insets.systemWindowInsetTop + (48 * density).toInt() + (28 * density).toInt()
|
||||
systemWindowInsetTop + (48 * density).toInt() + (28 * density).toInt()
|
||||
} else {
|
||||
insets.systemWindowInsetTop + (12 * density).toInt()
|
||||
systemWindowInsetTop + (12 * density).toInt()
|
||||
}
|
||||
binding.fabLayers.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = margin
|
||||
@@ -221,6 +223,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.layersSheet.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = margin
|
||||
}
|
||||
|
||||
// set map padding so that compass is not obstructed by toolbar
|
||||
mapTopPadding = systemWindowInsetTop + (48 * density).toInt() + (16 * density).toInt()
|
||||
// if we actually use map.setPadding here, Mapbox will re-trigger onApplyWindowInsets
|
||||
// and cause an infinite loop. So we rely on onMapReady being called later than
|
||||
// onApplyWindowInsets.
|
||||
|
||||
insets
|
||||
}
|
||||
|
||||
@@ -888,7 +897,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
map.setTrafficEnabled(vm.mapTrafficEnabled.value ?: false)
|
||||
|
||||
// set padding so that compass is not obstructed by toolbar
|
||||
map.setPadding(0, binding.toolbarContainer.height, 0, 0)
|
||||
map.setPadding(0, mapTopPadding, 0, 0)
|
||||
|
||||
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
map.setMapStyle(
|
||||
@@ -1041,7 +1050,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
// update icons of existing markers (connector filter may have changed)
|
||||
for ((marker, charger) in markers) {
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val highlight = charger.id == vm.chargerSparse.value?.id
|
||||
marker.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||
@@ -1065,7 +1074,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
// animate marker if it is visible, otherwise remove immediately
|
||||
if (bounds.contains(marker.position)) {
|
||||
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val highlight = charger.id == vm.chargerSparse.value?.id
|
||||
val fault = charger.faultReport != null
|
||||
val multi = charger.isMulti(vm.filteredConnectors.value)
|
||||
val fav =
|
||||
@@ -1085,7 +1094,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
for (charger in chargers) {
|
||||
if (!map1.contains(charger.id)) {
|
||||
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val highlight = charger.id == vm.chargerSparse.value?.id
|
||||
val fault = charger.faultReport != null
|
||||
val multi = charger.isMulti(vm.filteredConnectors.value)
|
||||
val fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.vonforst.evmap.storage
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.model.LatLng
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
@@ -86,11 +87,14 @@ class PreferenceDataSource(val context: Context) {
|
||||
context.getString(R.string.pref_map_provider_default)
|
||||
)!!
|
||||
|
||||
val searchProvider: String
|
||||
var searchProvider: String
|
||||
get() = sp.getString(
|
||||
"search_provider",
|
||||
context.getString(R.string.pref_search_provider_default)
|
||||
)!!
|
||||
set(value) {
|
||||
sp.edit().putString("search_provider", value).apply()
|
||||
}
|
||||
|
||||
var mapType: AnyMap.Type
|
||||
get() = AnyMap.Type.valueOf(sp.getString("map_type", null) ?: AnyMap.Type.NORMAL.toString())
|
||||
@@ -198,9 +202,41 @@ class PreferenceDataSource(val context: Context) {
|
||||
sp.edit().putLong("app_start_counter", value).apply()
|
||||
}
|
||||
|
||||
/** Counter for how many times the price comparison page was opened,
|
||||
* introduced with Version 1.3.4 **/
|
||||
var chargepriceCounter: Long
|
||||
get() = sp.getLong("chargeprice_counter", 0)
|
||||
set(value) {
|
||||
sp.edit().putLong("chargeprice_counter", value).apply()
|
||||
}
|
||||
|
||||
var opensourceDonationsDialogShown: Boolean
|
||||
get() = sp.getBoolean("opensource_donations_dialog_shown", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("opensource_donations_dialog_shown", value).apply()
|
||||
}
|
||||
|
||||
var placeSearchResultAndroidAuto: LatLng?
|
||||
get() = if (sp.contains("place_search_result_android_auto_lat")) {
|
||||
LatLng(
|
||||
Double.fromBits(sp.getLong("place_search_result_android_auto_lat", 0L)),
|
||||
Double.fromBits(sp.getLong("place_search_result_android_auto_lng", 0L))
|
||||
)
|
||||
} else null
|
||||
set(value) {
|
||||
if (value == null) {
|
||||
sp.edit().remove("place_search_result_android_auto_lat")
|
||||
.remove("place_search_result_android_auto_lng").apply()
|
||||
} else {
|
||||
sp.edit().putLong("place_search_result_android_auto_lat", value.latitude.toBits())
|
||||
.putLong("place_search_result_android_auto_lng", value.longitude.toBits())
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
var placeSearchResultAndroidAutoName: String?
|
||||
get() = sp.getString("place_search_result_android_auto_name", null)
|
||||
set(value) {
|
||||
sp.edit().putString("place_search_result_android_auto_name", value).apply()
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,19 @@ abstract class RecentAutocompletePlaceDao {
|
||||
limit: Int? = null
|
||||
): List<RecentAutocompletePlace>
|
||||
|
||||
@Query("SELECT * FROM recentautocompleteplace WHERE dataSource = :dataSource AND primaryText LIKE '%' || :query || '%' ORDER BY timestamp DESC LIMIT :limit")
|
||||
abstract suspend fun searchAsync(
|
||||
query: String,
|
||||
dataSource: String,
|
||||
limit: Int? = null
|
||||
): List<RecentAutocompletePlace>
|
||||
|
||||
@Query("SELECT * FROM recentautocompleteplace WHERE dataSource = :dataSource ORDER BY timestamp DESC LIMIT :limit")
|
||||
abstract fun getAll(dataSource: String, limit: Int? = null): List<RecentAutocompletePlace>
|
||||
|
||||
@Query("SELECT * FROM recentautocompleteplace WHERE dataSource = :dataSource ORDER BY timestamp DESC LIMIT :limit")
|
||||
abstract suspend fun getAllAsync(
|
||||
dataSource: String,
|
||||
limit: Int? = null
|
||||
): List<RecentAutocompletePlace>
|
||||
}
|
||||
@@ -86,4 +86,11 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
|
||||
// set selected profile
|
||||
prefs.filterStatus = profileId
|
||||
}
|
||||
|
||||
suspend fun deleteCurrentProfile() {
|
||||
filterProfile.value?.let {
|
||||
db.filterProfileDao().delete(it)
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,6 +434,11 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
chargerDetails.value = response
|
||||
if (response.status == Status.SUCCESS) {
|
||||
chargerSparse.value = response.data
|
||||
|
||||
if (response.data != null && favorites.value?.any { it.charger.id == response.data.id } == true) {
|
||||
// update data of stored favorite
|
||||
db.chargeLocationsDao().insert(response.data)
|
||||
}
|
||||
} else {
|
||||
chargerSparse.value = null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/chip_background" />
|
||||
<item android:drawable="@color/my_tariff_background" />
|
||||
<item android:drawable="?selectableItemBackground" />
|
||||
</layer-list>
|
||||
@@ -3,7 +3,8 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
@@ -18,7 +19,6 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:fitsSystemWindows="true"
|
||||
app:headerLayout="@layout/nav_header"
|
||||
app:menu="@menu/drawer" />
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
@@ -107,7 +107,7 @@
|
||||
android:id="@+id/textView4"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{@string/chargeprice_stats(vm.chargepriceMetaForChargepoint.data.energy, BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)), vm.chargepriceMetaForChargepoint.data.power)}"
|
||||
android:text="@{@string/chargeprice_stats(vm.chargepriceMetaForChargepoint.data.energy, BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)), vm.chargepriceMetaForChargepoint.data.energy / vm.chargepriceMetaForChargepoint.data.duration * 60)}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:invisibleUnlessAnimated="@{!vm.batteryRangeSliderDragging && vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView2"
|
||||
@@ -242,4 +242,4 @@
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
</layout>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="24dp"
|
||||
android:fitsSystemWindows="true"
|
||||
android:id="@+id/nav_header">
|
||||
|
||||
<include layout="@layout/app_logo" />
|
||||
|
||||
@@ -4,50 +4,54 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/nav_graph">
|
||||
|
||||
<fragment
|
||||
<navigation
|
||||
android:id="@+id/map"
|
||||
android:name="net.vonforst.evmap.fragment.MapFragment"
|
||||
android:label=""
|
||||
tools:layout="@layout/fragment_map">
|
||||
<action
|
||||
android:id="@+id/action_map_to_filterFragment"
|
||||
app:destination="@id/filter"
|
||||
app:exitAnim="@animator/nav_default_exit_anim"
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_filterProfilesFragment"
|
||||
app:destination="@id/filter_profiles"
|
||||
app:exitAnim="@animator/nav_default_exit_anim"
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_chargepriceFragment"
|
||||
app:destination="@id/chargeprice" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_opensource_donations"
|
||||
app:destination="@id/opensource_donations" />
|
||||
<argument
|
||||
android:name="locationName"
|
||||
android:defaultValue="@null"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
<argument
|
||||
android:name="chargerId"
|
||||
android:defaultValue="0L"
|
||||
app:argType="long" />
|
||||
<argument
|
||||
android:name="latLng"
|
||||
android:defaultValue="@null"
|
||||
app:argType="com.car2go.maps.model.LatLng"
|
||||
app:nullable="true" />
|
||||
<argument
|
||||
android:name="appStart"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
</fragment>
|
||||
app:startDestination="@id/map_frag">
|
||||
<fragment
|
||||
android:id="@+id/map_frag"
|
||||
android:name="net.vonforst.evmap.fragment.MapFragment"
|
||||
android:label=""
|
||||
tools:layout="@layout/fragment_map">
|
||||
<action
|
||||
android:id="@+id/action_map_to_filterFragment"
|
||||
app:destination="@id/filter"
|
||||
app:exitAnim="@animator/nav_default_exit_anim"
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_filterProfilesFragment"
|
||||
app:destination="@id/filter_profiles"
|
||||
app:exitAnim="@animator/nav_default_exit_anim"
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_chargepriceFragment"
|
||||
app:destination="@id/chargeprice" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_opensource_donations"
|
||||
app:destination="@id/opensource_donations" />
|
||||
<argument
|
||||
android:name="locationName"
|
||||
android:defaultValue="@null"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
<argument
|
||||
android:name="chargerId"
|
||||
android:defaultValue="0L"
|
||||
app:argType="long" />
|
||||
<argument
|
||||
android:name="latLng"
|
||||
android:defaultValue="@null"
|
||||
app:argType="com.car2go.maps.model.LatLng"
|
||||
app:nullable="true" />
|
||||
<argument
|
||||
android:name="appStart"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
</fragment>
|
||||
</navigation>
|
||||
<fragment
|
||||
android:id="@+id/about"
|
||||
android:name="net.vonforst.evmap.fragment.preference.AboutFragment"
|
||||
@@ -85,15 +89,19 @@
|
||||
android:name="net.vonforst.evmap.fragment.preference.AndroidAutoSettingsFragment"
|
||||
android:label="@string/settings_android_auto"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
<fragment
|
||||
<navigation
|
||||
android:id="@+id/favs"
|
||||
android:name="net.vonforst.evmap.fragment.FavoritesFragment"
|
||||
android:label="@string/menu_favs"
|
||||
tools:layout="@layout/fragment_favorites">
|
||||
<action
|
||||
android:id="@+id/action_favs_to_map"
|
||||
app:destination="@id/map" />
|
||||
</fragment>
|
||||
app:startDestination="@id/favs_frag">
|
||||
<fragment
|
||||
android:id="@+id/favs_frag"
|
||||
android:name="net.vonforst.evmap.fragment.FavoritesFragment"
|
||||
android:label="@string/menu_favs"
|
||||
tools:layout="@layout/fragment_favorites">
|
||||
<action
|
||||
android:id="@+id/action_favs_to_map"
|
||||
app:destination="@id/map" />
|
||||
</fragment>
|
||||
</navigation>
|
||||
<fragment
|
||||
android:id="@+id/filter"
|
||||
android:name="net.vonforst.evmap.fragment.FilterFragment"
|
||||
@@ -110,12 +118,15 @@
|
||||
android:label="@string/chargeprice_title"
|
||||
tools:layout="@layout/fragment_chargeprice">
|
||||
<action
|
||||
android:id="@+id/action_chargeprice_to_settingsFragment"
|
||||
app:destination="@id/settings"
|
||||
android:id="@+id/action_chargeprice_to_chargepriceSettingsFragment"
|
||||
app:destination="@id/settings_chargeprice"
|
||||
app:exitAnim="@animator/nav_default_exit_anim"
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_chargeprice_to_donateFragment"
|
||||
app:destination="@id/donate" />
|
||||
<argument
|
||||
android:name="charger"
|
||||
app:argType="net.vonforst.evmap.model.ChargeLocation" />
|
||||
|
||||
@@ -158,6 +158,8 @@
|
||||
<string name="welcome_2_detail">Du kannst die Farben im Menü unter “Über EVMap → FAQ” erneut ansehen)</string>
|
||||
<string name="donation_dialog_title">Danke, dass du EVMap nutzt!</string>
|
||||
<string name="donation_dialog_detail">EVMap ist kostenlos und Open Source, ich entwickle es in meiner Freizeit. Über GitHub kann jeder zur Weiterentwicklung der App beitragen. Durch die steigende Beliebtheit der App müssen allerdings auch laufende Kosten, z.B. für den Zugriff auf die Datenquellen, gedeckt werden. Daher freue ich mich auch über Spenden in der App oder über GitHub Sponsors.</string>
|
||||
<string name="chargeprice_donation_dialog_title">Du bist ein richtiger Sparfuchs!</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Es sieht so aus, als wenn du den Preisvergleich sehr gern nutzt. Für den Zugang zu den Preisinformationen muss der Entwickler von EVMap eine monatliche Gebühr an die Datenquelle Chargeprice.app zahlen. Um diesen Dienst weiter anbieten zu können, würde ich mich sehr über Spenden freuen.</string>
|
||||
<string name="deleted_filterprofile">„%s” gelöscht</string>
|
||||
<string name="undo">Rückgängig</string>
|
||||
<string name="rename">Umbenennen</string>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="background">#121212</color>
|
||||
<color name="my_tariff_background">#1FFFFFFF</color>
|
||||
</resources>
|
||||
@@ -25,6 +25,7 @@
|
||||
<color name="chargeprice_lock">#546E7A</color>
|
||||
<color name="chargeprice_star">#00C853</color>
|
||||
<color name="chip_background">#1F000000</color>
|
||||
<color name="my_tariff_background">#1F000000</color>
|
||||
<color name="background">#FFFFFF</color>
|
||||
<color name="pager_unselected">#1F000000</color>
|
||||
</resources>
|
||||
|
||||
@@ -157,6 +157,8 @@
|
||||
<string name="welcome_2_detail">(You can check the colors again under “About EVMap → FAQ” in the menu)</string>
|
||||
<string name="donation_dialog_title">Thank you for using EVMap!</string>
|
||||
<string name="donation_dialog_detail">EVMap is free and Open Source software that I develop in my spare time. Coding contributions on GitHub are very much appreciated. However, due to increasing popularity of the app, I also need to cover some running costs, e.g. for access to the data sources. Therefore, please consider supporting the app through a donation or via GitHub Sponsors.</string>
|
||||
<string name="chargeprice_donation_dialog_title">You\'re a real bargain hunter!</string>
|
||||
<string name="chargeprice_donation_dialog_detail">It seems like you like the price comparison feature a lot. To access the pricing data, the developer of EVMap needs to pay a monthly fee to the data provider Chargeprice.app. Therefore, please consider supporting EVMap through a donation.</string>
|
||||
<string name="deleted_filterprofile">Deleted “%s”</string>
|
||||
<string name="undo">Undo</string>
|
||||
<string name="rename">Rename</string>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
(e.g. in the debug version). -->
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:targetPackage="net.vonforst.evmap"
|
||||
android:targetPackage="${applicationId}"
|
||||
android:targetClass="net.vonforst.evmap.MapsActivity">
|
||||
<extra
|
||||
android:name="favorites"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import androidx.car.app.HandshakeInfo
|
||||
import androidx.car.app.testing.SessionController
|
||||
import androidx.car.app.testing.TestCarContext
|
||||
import androidx.car.app.testing.TestScreenManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.internal.DoNotInstrument
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@DoNotInstrument
|
||||
class CarAppTest {
|
||||
private val testCarContext =
|
||||
TestCarContext.createCarContext(ApplicationProvider.getApplicationContext()).apply {
|
||||
updateHandshakeInfo(HandshakeInfo("auto.testing", 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onCreateScreen_returnsExpectedScreen() {
|
||||
val service = Robolectric.setupService(CarAppService::class.java)
|
||||
val session = service.onCreateSession()
|
||||
val controller = SessionController(
|
||||
session, testCarContext,
|
||||
Intent().setComponent(
|
||||
ComponentName(testCarContext, CarAppService::class.java)
|
||||
)
|
||||
)
|
||||
controller.moveToState(Lifecycle.State.CREATED)
|
||||
val screenCreated =
|
||||
testCarContext.getCarService(TestScreenManager::class.java).screensPushed.last()
|
||||
|
||||
// location permission required
|
||||
assert(screenCreated is PermissionScreen)
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,18 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.6.21'
|
||||
ext.about_libs_version = '8.9.4'
|
||||
ext.nav_version = '2.4.2'
|
||||
ext.nav_version = '2.5.1'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
||||
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
|
||||
classpath "de.timfreiheit.resourceplaceholders:placeholders:0.4"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
12
fastlane/metadata/android/de-DE/changelogs/90.txt
Normal file
12
fastlane/metadata/android/de-DE/changelogs/90.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
Verbesserungen:
|
||||
- Android Auto: Ortssuche hinzugefügt
|
||||
|
||||
Fehler behoben:
|
||||
- Shortcut Favoriten: kein Wechsel zur Karte möglich
|
||||
- Favoritendaten wurden nicht bei Öffnen aktualisiert
|
||||
- Preisvergleich: Durchschnittsladeleistung falsch
|
||||
- Verfügbarkeit in kompakter Ansicht falsch (mit Filter nach Leistung)
|
||||
- Kompass hinter Pinhole
|
||||
- Abstürze behoben
|
||||
- Android Automotive: Verfügbarkeitsdaten wurden bei Refresh nicht neu geladen
|
||||
- Android Automotive: Fahrtrichtung wurde bei Fahrzeugdaten nicht angezeigt
|
||||
2
fastlane/metadata/android/de-DE/changelogs/92.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/92.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- OpenStreetMap/Mapbox: Karte ließ sich nicht mehr verschieben
|
||||
12
fastlane/metadata/android/en-US/changelogs/90.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/90.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
Improvements:
|
||||
- Android Auto: Added place search
|
||||
|
||||
Bugfixes:
|
||||
- Shortcut to favorites: switching to map not possible
|
||||
- Favorite data were not updated when viewing details
|
||||
- Price comparison: incorrect average power
|
||||
- Wrong availability data in compact view (with minimum power filter)
|
||||
- Compass hidden behind pinhole
|
||||
- Crashes fixed
|
||||
- Android Automotive: Availability data not updated on refresh
|
||||
- Android Automotive: Driving direction was not shown in vehicle data
|
||||
2
fastlane/metadata/android/en-US/changelogs/92.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/92.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- OpenStreetMap/Mapbox: Map was not movable
|
||||
@@ -1,21 +1,17 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
## For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
#
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
# Default value: -Xmx1024m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
#
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
#Sun Jul 24 11:49:27 CEST 2022
|
||||
kotlin.code.style=official
|
||||
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||
|
||||
Reference in New Issue
Block a user