Compare commits

...

38 Commits

Author SHA1 Message Date
johan12345
2e1e22a44b adjust disk size 2024-05-12 22:29:49 +02:00
johan12345
d638a88f4d update SDK 2024-05-12 18:57:02 +02:00
johan12345
b9c08a8e75 change screenshot size 2024-05-12 15:13:11 +02:00
johan12345
ab2a77d25a remove CleanStatusBar (not needed on ATD) 2024-05-12 14:33:45 +02:00
johan12345
e0ddc9f734 use androidx takeScreenshot 2024-05-10 17:52:28 +02:00
johan12345
168fa244d3 fix name of output file 2024-05-10 00:08:25 +02:00
johan12345
76388ccc6e no emulator metrics 2024-05-09 23:37:16 +02:00
johan12345
76aac7dae4 use ATD system image 2024-05-09 23:35:02 +02:00
johan12345
ee8b586079 implement automatic screenshot generation 2024-05-09 23:17:09 +02:00
Hosted Weblate
360e7767bd Translated using Weblate (Portuguese)
Currently translated at 100.0% (366 of 366 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2024-05-08 22:27:16 +02:00
Jean-Baptiste
5f0c9fd31d Enable automatic per app language (#340)
* Enable automatic per app language

* fix getAppLocale

---------

Co-authored-by: johan12345 <johan.forstner@gmail.com>
2024-05-08 22:19:09 +02:00
johan12345
536c884f23 fix crash introduced by 00b26d224f 2024-05-03 20:34:09 +02:00
johan12345
7daf5a0adb fix jumping position of slider in ChargepriceFragment
fixes #328
2024-04-28 19:30:57 +02:00
johan12345
862f2b06d8 remove broken resourcePlaceholders plugin, hardcode targetPackage in shortcuts.xml 2024-04-28 19:18:32 +02:00
johan12345
198a9ecc48 Fix snackbar action button color
fixes #337
2024-04-28 19:05:28 +02:00
johan12345
2762a32105 improve look of text input dialog
fixes #336
2024-04-28 18:52:41 +02:00
johan12345
8a83a80e75 Block ability to update filter profile name with nothing
fixes #335
2024-04-28 18:29:10 +02:00
johan12345
75e8569964 dismiss popupMenu when fragment is destroyed
fixes #331
2024-04-27 13:32:17 +02:00
johan12345
00b26d224f fix #329: Layer button reappears after screen rotation 2024-04-27 12:43:34 +02:00
Jean-Baptiste
836f42b299 Fix color of layer button (#334)
* Fix color of layer button

* keep the layers icon gray

---------

Co-authored-by: johan12345 <johan.forstner@gmail.com>
2024-04-27 12:24:19 +02:00
johan12345
3de994f09d update copyright in LICENSE 2024-04-26 22:41:39 +02:00
Jean-Baptiste
d78eda9d97 Update copyright (#333) 2024-04-26 22:40:45 +02:00
johan12345
ed4be05aed fix tests 2024-04-24 21:53:26 +02:00
johan12345
45de4c8ff0 Release 1.8.2 2024-04-24 21:21:00 +02:00
johan12345
b5b785be07 Czech strings CDATA 2024-04-24 21:17:21 +02:00
Hosted Weblate
0b8589d599 Translated using Weblate (Czech)
Currently translated at 100.0% (366 of 366 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (366 of 366 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/cs/
Translation: EVMap/Android
2024-04-24 21:16:36 +02:00
johan12345
ba757831f3 GoingElectricApi: catch cases where status != "ok" 2024-04-24 21:11:09 +02:00
johan12345
1990152836 Add option to disable native Chargeprice integration 2024-04-18 20:41:02 +02:00
johan12345
50fd433439 hide immediate navigation setting if Google Maps is not installed
fixes #326
2024-04-17 21:26:18 +02:00
johan12345
381e6f3d98 OCM: extract operator name using ReferenceData
fixes #323
2024-04-14 18:58:15 +02:00
johan12345
9c5582f19c update AnyMaps and android-spatialite
to make sure FORTIFY_SOURCE flag is used
2024-04-14 18:38:30 +02:00
johan12345
1c16d8cbb6 fix string 2024-04-07 16:02:18 +02:00
Hosted Weblate
1734f1c09e Translated using Weblate (Portuguese)
Currently translated at 100.0% (363 of 363 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2024-04-07 16:01:17 +02:00
Hosted Weblate
ebf0f82597 Translated using Weblate (Czech)
Currently translated at 100.0% (363 of 363 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (363 of 363 strings)

Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/cs/
Translation: EVMap/Android
2024-04-07 16:01:11 +02:00
Johan von Forstner
85a38a6da1 fix NPE during MapViewModel init 2024-04-05 14:42:36 +02:00
johan12345
60ca97179c update referral links 2024-03-17 19:14:04 +01:00
johan12345
4413cba9fa fix NPE? 2024-03-17 19:14:04 +01:00
Jean-Baptiste
e2e6a3060b Fix Node JS deprecations in Github Actions (#322)
* Bump setup-java to v4 in release action

* Bump setup-java to v4 in tests action
2024-01-31 21:08:36 +01:00
53 changed files with 919 additions and 183 deletions

View File

@@ -14,7 +14,7 @@ jobs:
uses: actions/checkout@v4
- name: Set up Java environment
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'zulu'

115
.github/workflows/screenshots.yml vendored Normal file
View File

@@ -0,0 +1,115 @@
on:
push:
branches:
- '*'
name: Generate Screenshots
jobs:
screenshot:
name: Generate screenshots
runs-on: ubuntu-latest
strategy:
matrix:
device:
- profile: pixel_6
api-level: 34
display_size: 1080x2336 # subtracted the 64px bottom navigation bar
type: phone
- profile: pixel_tablet
api-level: 34
display_size: 2560x1488 # subtracted the 64px navigation bar and 48px status bar
type: tenInch
env:
ANDROID_USER_HOME: /home/runner/.android
steps:
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Check out code
uses: actions/checkout@v4
- name: Retrieve debug keystore
env:
DEBUG_KEYSTORE_BASE64: ${{ secrets.DEBUG_KEYSTORE_BASE64 }}
run: |
mkdir ~/.config/.android
echo $DEBUG_KEYSTORE_BASE64 | base64 --decode > ~/.config/.android/debug.keystore
- name: Set up Java environment
uses: actions/setup-java@v3
with:
java-version: 17
distribution: 'zulu'
cache: 'gradle'
- name: Setup Android SDK
run: |
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "cmdline-tools;latest"
rm -r $ANDROID_HOME/cmdline-tools/latest
mv $ANDROID_HOME/cmdline-tools/latest-2 $ANDROID_HOME/cmdline-tools/latest
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --version
- name: AVD cache
uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: ${{ runner.os }}-avd-api${{ matrix.device.api-level }}-${{ matrix.device.profile }}
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.device.api-level }}
target: google_atd
arch: x86_64
profile: ${{ matrix.device.profile }}
force-avd-creation: false
ram-size: 2048M
disk-size: 4096M
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics
disable-animations: true
script: echo "Generated AVD snapshot for caching."
- name: Build app
run: ./gradlew assembleGoogleNormalDebug assembleGoogleNormalAndroidTest
env:
GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }}
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }}
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }}
ACRA_CRASHREPORT_CREDENTIALS: ${{ secrets.ACRA_CRASHREPORT_CREDENTIALS }}
- name: Run emulator and generate screenshots
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.device.api-level }}
target: google_atd
arch: x86_64
profile: ${{ matrix.device.profile }}
force-avd-creation: false
ram-size: 2048M
disk-size: 4096M
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics
disable-animations: true
script: |
adb shell pm clear net.vonforst.evmap.debug || true
adb shell wm size ${{ matrix.device.display_size }}
adb logcat -c
adb logcat *:E &
fastlane screengrab --app_apk_path app/build/outputs/apk/googleNormal/debug/app-google-normal-debug.apk --test_apk_path app/build/outputs/apk/androidTest/googleNormal/debug/app-google-normal-debug-androidTest.apk --tests_package_name=net.vonforst.evmap.debug.test --app_package_name net.vonforst.evmap.debug -p net.vonforst.evmap.screenshot --use_timestamp_suffix false --clear_previous_screenshots true --device_type=${{ matrix.device.type }} -q en-US,de-DE,fr-FR,nb-rNO,nl-NL,pt-PT,ro-RO
- name: Upload screenshots as artifacts
uses: actions/upload-artifact@v3
with:
name: screenshots-${{ matrix.device.profile }}-${{ matrix.device.api-level }}
path: fastlane/metadata/android

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4
- name: Set up Java environment
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'zulu'

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2023 Johan von Forstner and contributors
Copyright (c) 2020-2024 Johan von Forstner and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -8,10 +8,8 @@ plugins {
id("kotlin-kapt")
id("androidx.navigation.safeargs.kotlin")
id("com.mikepenz.aboutlibraries.plugin")
id("pt.jcosta.resourceplaceholders")
}
val supportedLocales = "en,de,fr,nb-rNO,nl,pt,ro,cs"
android {
defaultConfig {
@@ -20,12 +18,10 @@ android {
minSdk = 21
targetSdk = 34
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode = 210
versionName = "1.8.1"
versionCode = 212
versionName = "1.8.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
resourceConfigurations += supportedLocales.split(",")
buildConfigField("String", "supportedLocales", "\"$supportedLocales\"")
}
signingConfigs {
@@ -101,6 +97,9 @@ android {
disable += listOf("NullSafeMutableLiveData")
warning += listOf("MissingTranslation")
}
androidResources {
generateLocaleConfig = true
}
testOptions {
unitTests {
@@ -108,9 +107,6 @@ android {
}
}
resourcePlaceholders {
files("xml/shortcuts.xml")
}
namespace = "net.vonforst.evmap"
// add API keys from environment variable if not set in apikeys.xml
@@ -265,7 +261,7 @@ dependencies {
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
// AnyMaps
val anyMapsVersion = "60b6d4f821"
val anyMapsVersion = "4854581f72"
implementation("com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion")
googleImplementation("com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion")
googleImplementation("com.google.android.gms:play-services-maps:18.2.0")
@@ -280,6 +276,14 @@ dependencies {
// patched version that removes build-time dependency on GMS (-> no Google location services)
fossImplementation("com.github.ev-map:mapbox-events-android:a21c324501")
implementation("com.mapbox.mapboxsdk:mapbox-android-sdk") {
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-accounts")
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-telemetry")
version {
strictly("9.1.0-SNAPSHOT")
}
}
// Google Places
googleImplementation("com.google.android.libraries.places:places:3.3.0")
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
@@ -301,7 +305,10 @@ dependencies {
implementation("androidx.room:room-runtime:$room_version")
kapt("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$room_version")
implementation("com.github.anboralabs:spatia-room:0.2.7")
implementation("com.github.anboralabs:spatia-room:0.2.9") {
exclude(group = "com.github.dalgarins", module = "android-spatialite")
}
implementation("com.github.EV-map:android-spatialite:e5495c83ad") // version with minSdk increased to 21 & FORTIFY_SOURCE enabled
// billing library
val billing_version = "6.1.0"
@@ -318,6 +325,7 @@ dependencies {
debugImplementation("com.facebook.flipper:flipper:0.238.0")
debugImplementation("com.facebook.soloader:soloader:0.10.5")
debugImplementation("com.facebook.flipper:flipper-network-plugin:0.238.0")
debugImplementation("androidx.test.espresso:espresso-idling-resource:3.5.1")
// testing
testImplementation("junit:junit:4.13.2")
@@ -328,11 +336,14 @@ dependencies {
testImplementation("androidx.test:core:1.5.0")
testImplementation("androidx.arch.core:core-testing:2.2.0")
testImplementation("androidx.car.app:app-testing:$carAppVersion")
testImplementation("androidx.test:core:1.5.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1")
androidTestImplementation("androidx.test:rules:1.5.0")
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
androidTestImplementation("tools.fastlane:screengrab:2.1.1")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")

View File

@@ -0,0 +1,112 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.vonforst.evmap.screenshot
import android.view.View
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.FragmentActivity
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.IdlingResource
import androidx.test.ext.junit.rules.ActivityScenarioRule
import java.util.UUID
/**
* An espresso idling resource implementation that reports idle status for all data binding
* layouts. Data Binding uses a mechanism to post messages which Espresso doesn't track yet.
*
* Since this application only uses fragments, the resource only checks the fragments and their
* children instead of the whole view tree.
*
* Tracking bug: https://github.com/android/android-test/issues/317
*/
class DataBindingIdlingResource(
activityScenarioRule: ActivityScenarioRule<out FragmentActivity>
) : IdlingResource {
// list of registered callbacks
private val idlingCallbacks = mutableListOf<IdlingResource.ResourceCallback>()
// give it a unique id to workaround an espresso bug where you cannot register/unregister
// an idling resource w/ the same name.
private val id = UUID.randomUUID().toString()
// holds whether isIdle is called and the result was false. We track this to avoid calling
// onTransitionToIdle callbacks if Espresso never thought we were idle in the first place.
private var wasNotIdle = false
lateinit var activity: FragmentActivity
override fun getName() = "DataBinding $id"
init {
monitorActivity(activityScenarioRule.scenario)
}
override fun isIdleNow(): Boolean {
val idle = !getBindings().any { it.hasPendingBindings() }
@Suppress("LiftReturnOrAssignment")
if (idle) {
if (wasNotIdle) {
// notify observers to avoid espresso race detector
idlingCallbacks.forEach { it.onTransitionToIdle() }
}
wasNotIdle = false
} else {
wasNotIdle = true
// check next frame
activity.findViewById<View>(android.R.id.content).postDelayed({
isIdleNow
}, 16)
}
return idle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
idlingCallbacks.add(callback)
}
/**
* Sets the activity from an [ActivityScenario] to be used from [DataBindingIdlingResource].
*/
private fun monitorActivity(
activityScenario: ActivityScenario<out FragmentActivity>
) {
activityScenario.onActivity {
this.activity = it
}
}
/**
* Find all binding classes in all currently available fragments.
*/
private fun getBindings(): List<ViewDataBinding> {
val fragments = (activity as? FragmentActivity)
?.supportFragmentManager
?.fragments
val bindings =
fragments?.mapNotNull {
it.view?.getBinding()
} ?: emptyList()
val childrenBindings = fragments?.flatMap { it.childFragmentManager.fragments }
?.mapNotNull { it.view?.getBinding() } ?: emptyList()
return bindings + childrenBindings
}
}
private fun View.getBinding(): ViewDataBinding? = DataBindingUtil.getBinding(this)

View File

@@ -0,0 +1,155 @@
package net.vonforst.evmap.screenshot
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.content.Intent
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.pressBack
import androidx.test.espresso.contrib.DrawerActions
import androidx.test.espresso.contrib.NavigationViewActions
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.test.rule.GrantPermissionRule.grant
import kotlinx.coroutines.runBlocking
import net.vonforst.evmap.EXTRA_CHARGER_ID
import net.vonforst.evmap.EXTRA_LAT
import net.vonforst.evmap.EXTRA_LON
import net.vonforst.evmap.EspressoIdlingResource
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.api.goingelectric.GEReferenceData
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.PreferenceDataSource
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.locale.LocaleTestRule
import java.time.Instant
@RunWith(AndroidJUnit4::class)
class ScreenshotTest {
companion object {
@JvmStatic
@BeforeClass
fun beforeAll() {
IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource)
Screengrab.setDefaultScreenshotStrategy { screenshotName, screenshotCallback ->
screenshotCallback.screenshotCaptured(
screenshotName,
androidx.test.core.app.takeScreenshot()
)
}
val context = InstrumentationRegistry.getInstrumentation().targetContext
val prefs = PreferenceDataSource(context)
prefs.dataSourceSet = true
prefs.welcomeDialogShown = true
prefs.privacyAccepted = true
prefs.opensourceDonationsDialogLastShown = Instant.now()
prefs.chargepriceMyVehicles = setOf("b58bc94d-d929-ad71-d95b-08b877bf76ba")
prefs.appStartCounter = 0
prefs.mapProvider = "google"
// insert favorites
val db = AppDatabase.getInstance(context)
val api = GoingElectricApiWrapper(
context.getString(R.string.goingelectric_key),
context = context
)
val ids = listOf(70774L to true, 40315L to true, 65330L to true, 62489L to false)
runBlocking {
val refData = api.getReferenceData().data as GEReferenceData
ids.forEachIndexed { i, (id, favorite) ->
val detail = api.getChargepointDetail(refData, id).data!!
db.chargeLocationsDao().insert(detail)
if (db.favoritesDao().findFavorite(id, "goingelectric") == null && favorite) {
db.favoritesDao().insert(
Favorite(
chargerId = id,
chargerDataSource = "goingelectric"
)
)
}
}
}
}
}
@get:Rule
val localeTestRule = LocaleTestRule()
@get:Rule
val activityRule: ActivityScenarioRule<MapsActivity> = ActivityScenarioRule(
Intent(
InstrumentationRegistry.getInstrumentation().targetContext,
MapsActivity::class.java
).apply {
putExtra(EXTRA_CHARGER_ID, 62489L)
putExtra(EXTRA_LAT, 53.099512)
putExtra(EXTRA_LON, 9.981884)
})
@get:Rule
val permissionRule: GrantPermissionRule = grant(ACCESS_FINE_LOCATION)
@Before
fun setUp() {
IdlingRegistry.getInstance().register(DataBindingIdlingResource(activityRule))
}
@Test
fun testTakeScreenshot() {
Thread.sleep(15000L)
Screengrab.screenshot("01_map_google")
onView(withId(R.id.topPart)).perform(click())
Thread.sleep(1000L)
Screengrab.screenshot("02_detail")
onView(withId(R.id.btnChargeprice)).perform(click())
Thread.sleep(5000L)
Screengrab.screenshot("03_prices")
onView(isRoot()).perform(pressBack())
Thread.sleep(500L)
onView(isRoot()).perform(pressBack())
Thread.sleep(2000L)
onView(withId(R.id.menu_filter)).perform(click())
Thread.sleep(1000L)
onView(withText(R.string.menu_edit_filters)).perform(click())
Thread.sleep(1000L)
Screengrab.screenshot("05_filters")
onView(isRoot()).perform(pressBack())
onView(withId(R.id.drawer_layout)).perform(DrawerActions.open())
onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.favs))
Thread.sleep(10000L)
Screengrab.screenshot("04_favorites")
val context = InstrumentationRegistry.getInstrumentation().targetContext
PreferenceDataSource(context).mapProvider = "mapbox"
onView(isRoot()).perform(pressBack())
Thread.sleep(5000L)
Screengrab.screenshot("01_map_mapbox")
}
}

View File

@@ -1,4 +1,4 @@
package com.johan.evmap.storage
package net.vonforst.evmap.storage
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule

View File

@@ -1,5 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions needed for fastlane screengrab -->
<!-- Allows unlocking your device and activating its screen so UI tests can succeed -->
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Allows for storing and retrieving screenshots -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Allows changing locales -->
<uses-permission
android:name="android.permission.CHANGE_CONFIGURATION"
tools:ignore="ProtectedPermissions" />
<!-- Allows changing SystemUI demo mode -->
<uses-permission
android:name="android.permission.DUMP"
tools:ignore="ProtectedPermissions" />
<application>
<activity

View File

@@ -2,6 +2,8 @@ package net.vonforst.evmap
import android.content.Context
import android.os.Build
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.idling.CountingIdlingResource
import com.facebook.flipper.android.AndroidFlipperClient
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
import com.facebook.flipper.plugins.inspector.DescriptorMapping
@@ -34,9 +36,29 @@ fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {
} catch (e: ClassNotFoundException) {
isRunningTest = false
}
if (!isRunningTest) {
this.addNetworkInterceptor(FlipperOkhttpInterceptor(networkFlipperPlugin))
}
return this
}
/**
* Contains a static reference to [IdlingResource], only available in the 'debug' build type.
*/
object EspressoIdlingResource {
private const val RESOURCE = "GLOBAL"
@JvmField
val countingIdlingResource = CountingIdlingResource(RESOURCE)
fun increment() {
countingIdlingResource.increment()
}
fun decrement() {
if (!countingIdlingResource.isIdleNow) {
countingIdlingResource.decrement()
}
}
}

View File

@@ -27,6 +27,7 @@
<package android:name="com.google.android.projection.gearhead" />
<package android:name="com.google.android.apps.automotive.templates.host" />
<package android:name="com.google.android.apps.maps" />
</queries>
<application
@@ -40,8 +41,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:localeConfig="@xml/locales_config">
android:theme="@style/AppTheme">
<meta-data
android:name="com.mapbox.ACCESS_TOKEN"

View File

@@ -1,5 +1,6 @@
package net.vonforst.evmap
import android.app.ActivityOptions
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Intent
@@ -206,18 +207,15 @@ class MapsActivity : AppCompatActivity(),
}
}
} else if (intent.hasExtra(EXTRA_CHARGER_ID)) {
deepLink = navController.createDeepLink()
.setDestination(R.id.map)
.setArguments(
MapFragmentArgs(
chargerId = intent.getLongExtra(EXTRA_CHARGER_ID, 0),
latLng = LatLng(
intent.getDoubleExtra(EXTRA_LAT, 0.0),
intent.getDoubleExtra(EXTRA_LON, 0.0)
)
).toBundle()
)
.createPendingIntent()
navController.navigate(
R.id.map, MapFragmentArgs(
chargerId = intent.getLongExtra(EXTRA_CHARGER_ID, 0),
latLng = LatLng(
intent.getDoubleExtra(EXTRA_LAT, 0.0),
intent.getDoubleExtra(EXTRA_LON, 0.0)
)
).toBundle()
)
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
@@ -268,7 +266,7 @@ class MapsActivity : AppCompatActivity(),
}
}
fun openUrl(url: String) {
fun openUrl(url: String, preferBrowser: Boolean = true) {
val pkg = CustomTabsClient.getPackageName(this, null)
val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
@@ -280,7 +278,7 @@ class MapsActivity : AppCompatActivity(),
pkg?.let {
// prefer to open URL in custom tab, even if native app
// available (such as EVMap itself)
intent.intent.setPackage(pkg)
if (preferBrowser) intent.intent.setPackage(pkg)
}
try {
intent.launchUrl(this, Uri.parse(url))

View File

@@ -1,6 +1,7 @@
package net.vonforst.evmap
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
@@ -121,4 +122,21 @@ fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): Pa
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong()))
} else {
@Suppress("DEPRECATION") getPackageInfo(packageName, flags)
}
}
fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int = 0): ApplicationInfo =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(flags.toLong()))
} else {
@Suppress("DEPRECATION") getApplicationInfo(packageName, flags)
}
fun PackageManager.isAppInstalled(packageName: String): Boolean {
return try {
getApplicationInfoCompat(packageName, 0).enabled
} catch (e: PackageManager.NameNotFoundException) {
false
}
}

View File

@@ -12,8 +12,28 @@ import kotlinx.coroutines.withContext
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.*
import net.vonforst.evmap.model.*
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.ChargepointList
import net.vonforst.evmap.api.FiltersSQLQuery
import net.vonforst.evmap.api.StringProvider
import net.vonforst.evmap.api.mapPower
import net.vonforst.evmap.api.mapPowerInverse
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.powerSteps
import net.vonforst.evmap.model.BooleanFilter
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.model.ChargepointListItem
import net.vonforst.evmap.model.Filter
import net.vonforst.evmap.model.FilterValue
import net.vonforst.evmap.model.FilterValues
import net.vonforst.evmap.model.MultipleChoiceFilter
import net.vonforst.evmap.model.MultipleChoiceFilterValue
import net.vonforst.evmap.model.ReferenceData
import net.vonforst.evmap.model.SliderFilter
import net.vonforst.evmap.model.getBooleanValue
import net.vonforst.evmap.model.getMultipleChoiceValue
import net.vonforst.evmap.model.getSliderValue
import net.vonforst.evmap.viewmodel.Resource
import net.vonforst.evmap.viewmodel.getClusterDistance
import okhttp3.Cache
@@ -22,7 +42,11 @@ import retrofit2.HttpException
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.*
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import java.io.IOException
import java.time.Duration
@@ -123,6 +147,8 @@ interface GoingElectricApi {
}
}
private const val STATUS_OK = "ok"
class GoingElectricApiWrapper(
val apikey: String,
baseurl: String = "https://api.goingelectric.de",
@@ -211,11 +237,11 @@ class GoingElectricApiWrapper(
categories = categories,
startkey = startkey
)
if (!response.isSuccessful || response.body()!!.status != "ok") {
if (!response.isSuccessful || response.body()!!.status != STATUS_OK) {
return Resource.error(response.message(), null)
} else {
val body = response.body()!!
data.addAll(body.chargelocations)
data.addAll(body.chargelocations!!)
startkey = body.startkey
}
} catch (e: IOException) {
@@ -308,11 +334,11 @@ class GoingElectricApiWrapper(
categories = categories,
startkey = startkey
)
if (!response.isSuccessful || response.body()!!.status != "ok") {
if (!response.isSuccessful || response.body()!!.status != STATUS_OK) {
return Resource.error(response.message(), null)
} else {
val body = response.body()!!
data.addAll(body.chargelocations)
data.addAll(body.chargelocations!!)
startkey = body.startkey
}
} catch (e: IOException) {
@@ -393,9 +419,9 @@ class GoingElectricApiWrapper(
): Resource<ChargeLocation> {
try {
val response = api.getChargepointDetail(id)
return if (response.isSuccessful && response.body()!!.status == "ok" && response.body()!!.chargelocations.size == 1) {
return if (response.isSuccessful && response.body()!!.status == STATUS_OK && response.body()!!.chargelocations!!.size == 1) {
Resource.success(
(response.body()!!.chargelocations[0] as GEChargeLocation).convert(
(response.body()!!.chargelocations!![0] as GEChargeLocation).convert(
apikey, true
)
)
@@ -423,16 +449,19 @@ class GoingElectricApiWrapper(
val responses = listOf(plugsResponse, chargeCardsResponse, networksResponse)
if (responses.map { it.isSuccessful }.all { it }) {
if (responses.map { it.isSuccessful }.all { it }
&& plugsResponse.body()!!.status == STATUS_OK
&& chargeCardsResponse.body()!!.status == STATUS_OK
&& networksResponse.body()!!.status == STATUS_OK) {
Resource.success(
GEReferenceData(
plugsResponse.body()!!.result,
networksResponse.body()!!.result,
chargeCardsResponse.body()!!.result
plugsResponse.body()!!.result!!,
networksResponse.body()!!.result!!,
chargeCardsResponse.body()!!.result!!
)
)
} else {
Resource.error(responses.find { !it.isSuccessful }!!.message(), null)
Resource.error(responses.find { !it.isSuccessful }?.message(), null)
}
} catch (e: IOException) {
Resource.error(e.message, null)

View File

@@ -27,20 +27,20 @@ import java.time.LocalTime
@JsonClass(generateAdapter = true)
data class GEChargepointList(
val status: String,
val chargelocations: List<GEChargepointListItem>,
val chargelocations: List<GEChargepointListItem>?,
@JsonObjectOrFalse val startkey: Int?
)
@JsonClass(generateAdapter = true)
data class GEStringList(
val status: String,
val result: List<String>
val result: List<String>?
)
@JsonClass(generateAdapter = true)
data class GEChargeCardList(
val status: String,
val result: List<GEChargeCard>
val result: List<GEChargeCard>?
)
sealed class GEChargepointListItem {

View File

@@ -8,8 +8,26 @@ import com.squareup.moshi.Moshi
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.*
import net.vonforst.evmap.model.*
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.ChargepointList
import net.vonforst.evmap.api.FiltersSQLQuery
import net.vonforst.evmap.api.StringProvider
import net.vonforst.evmap.api.mapPower
import net.vonforst.evmap.api.mapPowerInverse
import net.vonforst.evmap.api.powerSteps
import net.vonforst.evmap.model.BooleanFilter
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.ChargepointListItem
import net.vonforst.evmap.model.Filter
import net.vonforst.evmap.model.FilterValue
import net.vonforst.evmap.model.FilterValues
import net.vonforst.evmap.model.MultipleChoiceFilter
import net.vonforst.evmap.model.MultipleChoiceFilterValue
import net.vonforst.evmap.model.ReferenceData
import net.vonforst.evmap.model.SliderFilter
import net.vonforst.evmap.model.getBooleanValue
import net.vonforst.evmap.model.getMultipleChoiceValue
import net.vonforst.evmap.model.getSliderValue
import net.vonforst.evmap.viewmodel.Resource
import okhttp3.Cache
import okhttp3.OkHttpClient
@@ -390,9 +408,7 @@ class OpenChargeMapApiWrapper(
}
override fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean {
val operators = filters.getMultipleChoiceValue("operators")
return (operators != null && !operators.all)
// TODO: it would be possible to implement this without requiring details if we extended the data structure to also save the operator ID in the DB
return false
}
}

View File

@@ -63,7 +63,7 @@ data class OCMChargepoint(
Coordinate(addressInfo.latitude, addressInfo.longitude),
addressInfo.toAddress(refData),
connections.map { it.convert(refData) },
operatorInfo?.title,
operatorInfo?.title ?: refData.operators.find { it.id == operatorId }?.title,
"https://map.openchargemap.io/?id=$id",
"https://map.openchargemap.io/?id=$id",
convertFaultReport(),

View File

@@ -1,7 +1,11 @@
package net.vonforst.evmap.auto
import android.content.Intent
import android.graphics.*
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.text.SpannableString
@@ -11,7 +15,17 @@ 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.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarIconSpan
import androidx.car.app.model.ForegroundCarColorSpan
import androidx.car.app.model.Pane
import androidx.car.app.model.PaneTemplate
import androidx.car.app.model.ParkedOnlyOnClickListener
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.scale
import androidx.core.text.HtmlCompat
@@ -22,13 +36,17 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.*
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.EXTRA_CHARGER_ID
import net.vonforst.evmap.EXTRA_LAT
import net.vonforst.evmap.EXTRA_LON
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.formatTeslaParkingFee
import net.vonforst.evmap.adapter.formatTeslaPricing
import net.vonforst.evmap.api.availability.AvailabilityRepository
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.tesla.Pricing
import net.vonforst.evmap.api.availability.tesla.TeslaChargingOwnershipGraphQlApi
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.fronyx.FronyxApi
@@ -41,6 +59,7 @@ import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Cost
import net.vonforst.evmap.model.FaultReport
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.plus
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.PreferenceDataSource
@@ -131,7 +150,16 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
)
.setTitle(carContext.getString(R.string.auto_prices))
.setOnClickListener {
screenManager.push(ChargepriceScreen(carContext, charger))
if (prefs.chargepriceNativeIntegration) {
screenManager.push(ChargepriceScreen(carContext, charger))
} else {
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse(ChargepriceApi.getPoiUrl(charger))
)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
carContext.startActivity(intent)
}
}
.build())
}

View File

@@ -14,14 +14,30 @@ import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.*
import androidx.car.app.model.Action
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.GridItem
import androidx.car.app.model.GridTemplate
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.ParkedOnlyOnClickListener
import androidx.car.app.model.Row
import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import androidx.car.app.model.Toggle
import androidx.core.content.IntentCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.text.HtmlCompat
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import kotlinx.coroutines.launch
import net.vonforst.evmap.*
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.EXTRA_DONATE
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
import net.vonforst.evmap.api.availability.tesla.TeslaOwnerApi
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
@@ -29,6 +45,7 @@ import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
import net.vonforst.evmap.getPackageInfoCompat
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
import net.vonforst.evmap.storage.PreferenceDataSource
@@ -408,12 +425,21 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
setTitle(carContext.getString(R.string.settings_chargeprice))
setHeaderAction(Action.BACK)
setSingleList(ItemList.Builder().apply {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_chargeprice_native_integration))
addText(carContext.getString(if (prefs.chargepriceNativeIntegration) R.string.pref_chargeprice_native_integration_on else R.string.pref_chargeprice_native_integration_off))
setToggle(Toggle.Builder {
prefs.chargepriceNativeIntegration = it
invalidate()
}.setChecked(prefs.chargepriceNativeIntegration).build())
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_my_vehicle))
setBrowsable(true)
setOnClickListener {
screenManager.push(SelectVehiclesScreen(carContext))
}
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_my_tariffs))
@@ -437,6 +463,7 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
)
}
)
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.settings_android_auto_chargeprice_range))
@@ -454,6 +481,7 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
setOnClickListener {
screenManager.push(SelectChargingRangeScreen(carContext))
}
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_chargeprice_currency))
@@ -469,27 +497,31 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
setOnClickListener {
screenManager.push(SelectCurrencyScreen(carContext))
}
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_chargeprice_no_base_fee))
setToggle(Toggle.Builder {
prefs.chargepriceNoBaseFee = it
}.setChecked(prefs.chargepriceNoBaseFee).build())
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs))
addText(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs_summary))
setToggle(Toggle.Builder {
prefs.chargepriceShowProviderCustomerTariffs = it
}.setChecked(prefs.chargepriceShowProviderCustomerTariffs).build())
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
if (maxRows > 6) {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs))
addText(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs_summary))
setToggle(Toggle.Builder {
prefs.chargepriceShowProviderCustomerTariffs = it
}.setChecked(prefs.chargepriceShowProviderCustomerTariffs).build())
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.pref_chargeprice_allow_unbalanced_load))
addText(carContext.getString(R.string.pref_chargeprice_allow_unbalanced_load_summary))
setToggle(Toggle.Builder {
prefs.chargepriceAllowUnbalancedLoad = it
}.setChecked(prefs.chargepriceAllowUnbalancedLoad).build())
setEnabled(prefs.chargepriceNativeIntegration)
}.build())
}
}.build())

View File

@@ -22,5 +22,8 @@ abstract class DonateFragmentBase : Fragment() {
referrals.referralEwieeinfach.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(getString(R.string.ewieeinfach_referral_link))
}
referrals.referralEprimo.setOnClickListener {
(requireActivity() as MapsActivity).openUrl(getString(R.string.eprimo_referral_link))
}
}
}

View File

@@ -1,7 +1,12 @@
package net.vonforst.evmap.fragment
import android.os.Bundle
import android.view.*
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
import androidx.databinding.DataBindingUtil
@@ -108,31 +113,19 @@ class FilterFragment : Fragment(), MenuProvider {
}
}
private fun saveProfile(error: Boolean = false) {
showEditTextDialog(requireContext()) { dialog, input ->
private fun saveProfile() {
showEditTextDialog(requireContext(), { dialog, input ->
vm.filterProfile.value?.let { profile ->
input.setText(profile.name)
}
if (error) {
input.error = getString(R.string.required)
}
dialog.setTitle(R.string.save_as_profile)
.setMessage(R.string.save_profile_enter_name)
.setPositiveButton(R.string.ok) { _, _ ->
if (input.text.isBlank()) {
saveProfile(true)
} else {
lifecycleScope.launch {
vm.saveAsProfile(input.text.toString())
findNavController().popBackStack()
}
}
}
.setNegativeButton(R.string.cancel) { _, _ ->
}
}
}, {
lifecycleScope.launch {
vm.saveAsProfile(it)
findNavController().popBackStack()
}
})
}
}

View File

@@ -183,20 +183,16 @@ class FilterProfilesFragment : Fragment() {
adapter = FilterProfilesAdapter(touchHelper, onDelete = { fp ->
delete(fp)
}, onRename = { fp ->
showEditTextDialog(requireContext()) { dialog, input ->
showEditTextDialog(requireContext(), { dialog, input ->
input.setText(fp.name)
dialog.setTitle(R.string.rename)
.setMessage(R.string.save_profile_enter_name)
.setPositiveButton(R.string.ok) { _, _ ->
lifecycleScope.launch {
vm.update(fp.copy(name = input.text.toString()))
}
}
.setNegativeButton(R.string.cancel) { _, _ ->
}
}
}, {
lifecycleScope.launch {
vm.update(fp.copy(name = it))
}
})
})
binding.filterProfilesList.apply {
this.adapter = this@FilterProfilesFragment.adapter

View File

@@ -11,7 +11,14 @@ import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.text.method.KeyListener
import android.view.*
import android.view.ContextThemeWrapper
import android.view.Gravity
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.AdapterView
import android.widget.ImageView
@@ -23,7 +30,13 @@ import androidx.annotation.RequiresPermission
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.location.LocationListenerCompat
import androidx.core.view.*
import androidx.core.view.MenuCompat
import androidx.core.view.MenuProvider
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.doOnLayout
import androidx.core.view.doOnNextLayout
import androidx.core.view.updateLayoutParams
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@@ -31,7 +44,6 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@@ -57,7 +69,12 @@ import com.google.android.material.transition.MaterialContainerTransform.FADE_MO
import com.google.android.material.transition.MaterialFadeThrough
import com.google.android.material.transition.MaterialSharedAxis
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.*
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.from
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
import com.stfalcon.imageviewer.StfalconImageViewer
import io.michaelrocks.bimap.HashBiMap
@@ -72,6 +89,7 @@ import net.vonforst.evmap.adapter.ConnectorAdapter
import net.vonforst.evmap.adapter.DetailsAdapter
import net.vonforst.evmap.adapter.GalleryAdapter
import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.autocomplete.ApiUnavailableException
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.bold
@@ -79,17 +97,35 @@ import net.vonforst.evmap.databinding.FragmentMapBinding
import net.vonforst.evmap.location.FusionEngine
import net.vonforst.evmap.location.LocationEngine
import net.vonforst.evmap.location.Priority
import net.vonforst.evmap.model.*
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.ChargeLocationCluster
import net.vonforst.evmap.model.ChargepointListItem
import net.vonforst.evmap.model.ChargerPhoto
import net.vonforst.evmap.model.FILTERS_CUSTOM
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.navigation.safeNavigate
import net.vonforst.evmap.shouldUseImperialUnits
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.*
import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.ClusterIconGenerator
import net.vonforst.evmap.ui.HideOnScrollFabBehavior
import net.vonforst.evmap.ui.MarkerAnimator
import net.vonforst.evmap.ui.chargerZ
import net.vonforst.evmap.ui.clusterZ
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.ui.placeSearchZ
import net.vonforst.evmap.ui.setTouchModal
import net.vonforst.evmap.utils.boundingBox
import net.vonforst.evmap.utils.checkAnyLocationPermission
import net.vonforst.evmap.utils.checkFineLocationPermission
import net.vonforst.evmap.utils.distanceBetween
import net.vonforst.evmap.utils.formatDecimal
import net.vonforst.evmap.viewmodel.*
import net.vonforst.evmap.viewmodel.GalleryViewModel
import net.vonforst.evmap.viewmodel.MapPosition
import net.vonforst.evmap.viewmodel.MapViewModel
import net.vonforst.evmap.viewmodel.Resource
import net.vonforst.evmap.viewmodel.Status
import java.io.IOException
import java.time.Duration
import java.time.Instant
@@ -119,6 +155,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private var connectionErrorSnackbar: Snackbar? = null
private var previousChargepointIds: Set<Long>? = null
private var mapTopPadding: Int = 0
private var popupMenu: PopupMenu? = null
private lateinit var clusterIconGenerator: ClusterIconGenerator
private lateinit var chargerIconGenerator: ChargerIconGenerator
@@ -399,12 +436,16 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
binding.detailView.btnChargeprice.setOnClickListener {
val charger = vm.charger.value?.data ?: return@setOnClickListener
val extras =
FragmentNavigatorExtras(binding.detailView.btnChargeprice to getString(R.string.shared_element_chargeprice))
findNavController().safeNavigate(
MapFragmentDirections.actionMapToChargepriceFragment(charger),
extras
)
if (prefs.chargepriceNativeIntegration) {
val extras =
FragmentNavigatorExtras(binding.detailView.btnChargeprice to getString(R.string.shared_element_chargeprice))
findNavController().safeNavigate(
MapFragmentDirections.actionMapToChargepriceFragment(charger),
extras
)
} else {
(activity as? MapsActivity)?.openUrl(ChargepriceApi.getPoiUrl(charger), false)
}
}
binding.detailView.btnChargerWebsite.setOnClickListener {
val charger = vm.charger.value?.data ?: return@setOnClickListener
@@ -689,6 +730,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
displaySearchResult(place, moveCamera = true)
}
vm.layersMenuOpen.observe(viewLifecycleOwner) { open ->
HideOnScrollFabBehavior.from(binding.fabLayers)?.hidden = open
binding.fabLayers.visibility = if (open) View.INVISIBLE else View.VISIBLE
binding.layersSheet.visibility = if (open) View.VISIBLE else View.INVISIBLE
updateBackPressedCallback()
@@ -1359,14 +1401,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
MenuCompat.setGroupDividerEnabled(popup.menu, true)
popup.setForceShowIcon(true)
popup.setOnMenuItemClickListener {
val navController = requireView().findNavController()
when (it.itemId) {
R.id.menu_edit_filters -> {
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
lifecycleScope.launch {
vm.copyFiltersToCustom()
navController.safeNavigate(
findNavController().safeNavigate(
MapFragmentDirections.actionMapToFilterFragment()
)
}
@@ -1376,7 +1417,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
R.id.menu_manage_filter_profiles -> {
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
navController.safeNavigate(
findNavController().safeNavigate(
MapFragmentDirections.actionMapToFilterProfilesFragment()
)
true
@@ -1456,6 +1497,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
}
popup.setTouchModal(false)
popupMenu = popup
popup.show()
}
@@ -1539,5 +1581,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
override fun onDestroy() {
super.onDestroy()
/* if we don't dismiss the popup menu, it will be recreated in some cases
(split-screen mode) and then have references to a destroyed fragment. */
popupMenu?.dismiss()
}
}

View File

@@ -7,6 +7,7 @@ import android.text.Spanned
import android.text.style.RelativeSizeSpan
import android.view.View
import androidx.fragment.app.viewModels
import androidx.preference.CheckBoxPreference
import net.vonforst.evmap.R
import net.vonforst.evmap.ui.MultiSelectDialogPreference
import net.vonforst.evmap.viewmodel.SettingsViewModel
@@ -28,9 +29,11 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
private lateinit var myVehiclePreference: MultiSelectDialogPreference
private lateinit var myTariffsPreference: MultiSelectDialogPreference
private lateinit var nativeIntegrationPreference: CheckBoxPreference
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
nativeIntegrationPreference = findPreference("chargeprice_native_integration")!!
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
myVehiclePreference.isEnabled = false
@@ -48,7 +51,7 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
)
}
}.toTypedArray()
myVehiclePreference.isEnabled = true
myVehiclePreference.isEnabled = nativeIntegrationPreference.isChecked
updateMyVehiclesSummary()
}
}
@@ -65,10 +68,28 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
it.name
}
}.toTypedArray()
myTariffsPreference.isEnabled = true
myTariffsPreference.isEnabled = nativeIntegrationPreference.isChecked
updateMyTariffsSummary()
}
}
updateNativeIntegrationState()
}
private fun updateNativeIntegrationState() {
for (i in 0 until preferenceScreen.preferenceCount) {
val pref = preferenceScreen.getPreference(i)
if (pref == nativeIntegrationPreference) {
continue
} else if (pref == myTariffsPreference) {
pref.isEnabled =
nativeIntegrationPreference.isChecked && vm.tariffs.value?.data != null
} else if (pref == myVehiclePreference) {
pref.isEnabled =
nativeIntegrationPreference.isChecked && vm.tariffs.value?.data != null
} else {
pref.isEnabled = nativeIntegrationPreference.isChecked
}
}
}
private fun updateMyTariffsSummary() {
@@ -110,6 +131,10 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
"chargeprice_my_tariffs" -> {
updateMyTariffsSummary()
}
"chargeprice_native_integration" -> {
updateNativeIntegrationState()
}
}
}
}

View File

@@ -6,9 +6,11 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import net.vonforst.evmap.R
import net.vonforst.evmap.isAppInstalled
import net.vonforst.evmap.ui.getAppLocale
import net.vonforst.evmap.ui.updateAppLocale
import net.vonforst.evmap.ui.updateNightMode
@@ -16,6 +18,7 @@ import net.vonforst.evmap.ui.updateNightMode
class UiSettingsFragment : BaseSettingsFragment() {
override val isTopLevel = false
lateinit var langPref: ListPreference
lateinit var immediateNavPref: CheckBoxPreference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_ui, rootKey)
@@ -28,11 +31,18 @@ class UiSettingsFragment : BaseSettingsFragment() {
val appLinkPref = findPreference<Preference>("applink_associate")!!
appLinkPref.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
immediateNavPref = findPreference("navigate_use_maps")!!
immediateNavPref.isVisible = isGoogleMapsInstalled()
}
private fun isGoogleMapsInstalled() =
requireContext().packageManager.isAppInstalled("com.google.android.apps.maps")
override fun onResume() {
super.onResume()
langPref.value = getAppLocale()
langPref.value = getAppLocale(requireContext())
immediateNavPref.isVisible = isGoogleMapsInstalled()
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {

View File

@@ -108,11 +108,14 @@ class PreferenceDataSource(val context: Context) {
val darkmode: String
get() = sp.getString("darkmode", "default")!!
val mapProvider: String
var mapProvider: String
get() = sp.getString(
"map_provider",
context.getString(R.string.pref_map_provider_default)
)!!
set(value) {
sp.edit().putString("map_provider", value).apply()
}
var searchProvider: String
get() = sp.getString(
@@ -147,6 +150,12 @@ class PreferenceDataSource(val context: Context) {
sp.edit().putBoolean("update_0.6.0_androidauto_dialog_shown", value).apply()
}
var chargepriceNativeIntegration: Boolean
get() = sp.getBoolean("chargeprice_native_integration", true)
set(value) {
sp.edit().putBoolean("chargeprice_native_integration", value).apply()
}
var chargepriceMyVehicles: Set<String>
get() = try {
sp.getStringSet("chargeprice_my_vehicle", emptySet())!!

View File

@@ -26,9 +26,13 @@ import androidx.viewpager2.widget.ViewPager2
import coil.load
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.slider.RangeSlider
import net.vonforst.evmap.*
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.iconForPlugType
import net.vonforst.evmap.isDarkMode
import net.vonforst.evmap.kmPerMile
import net.vonforst.evmap.meterPerFt
import net.vonforst.evmap.shouldUseImperialUnits
import java.time.Instant
import kotlin.math.ceil
import kotlin.math.floor
@@ -69,7 +73,7 @@ fun invisibleUnlessAnimated(view: View, oldValue: Boolean, newValue: Boolean) {
if (oldValue == newValue) {
if (!newValue && view.visibility == View.VISIBLE && view.alpha == 1f) {
// view is initially invisible
view.visibility = View.GONE
view.visibility = View.INVISIBLE
} else {
return
}

View File

@@ -1,10 +1,12 @@
package net.vonforst.evmap.ui
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.storage.PreferenceDataSource
fun updateNightMode(prefs: PreferenceDataSource) {
AppCompatDelegate.setDefaultNightMode(
when (prefs.darkmode) {
@@ -25,13 +27,14 @@ fun updateAppLocale(language: String) {
)
}
fun getAppLocale(): String? {
fun getAppLocale(context: Context): String? {
val locales = AppCompatDelegate.getApplicationLocales()
return if (locales.isEmpty) {
"default"
} else {
val arr = Array(locales.size()) { locales.get(it)!!.toLanguageTag() }
LocaleListCompat.forLanguageTags(BuildConfig.supportedLocales).getFirstMatch(arr)
?.toLanguageTag()
val choices =
context.resources.getStringArray(R.array.pref_language_values).joinToString(",")
LocaleListCompat.forLanguageTags(choices).getFirstMatch(arr)?.toLanguageTag()
}
}
}

View File

@@ -10,50 +10,60 @@ import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.FrameLayout
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import net.vonforst.evmap.R
import kotlin.math.roundToInt
private fun dialogEditText(ctx: Context): Pair<View, EditText> {
val container = FrameLayout(ctx)
container.setPadding(
(16 * ctx.resources.displayMetrics.density).toInt(), 0,
(16 * ctx.resources.displayMetrics.density).toInt(), 0
)
val input = EditText(ctx)
input.isSingleLine = true
container.addView(input)
return container to input
private fun dialogEditText(ctx: Context): Pair<TextInputLayout, EditText> {
val view = LayoutInflater.from(ctx).inflate(R.layout.dialog_textinput, null)
return view as TextInputLayout to view.findViewById(R.id.input)
}
fun showEditTextDialog(
ctx: Context,
customize: (MaterialAlertDialogBuilder, EditText) -> Unit
customize: (MaterialAlertDialogBuilder, EditText) -> Unit,
okAction: (String) -> Unit
): AlertDialog {
val (container, input) = dialogEditText(ctx)
val dialogBuilder = MaterialAlertDialogBuilder(ctx)
.setView(container)
.setPositiveButton(R.string.ok) { _, _ -> }
.setNegativeButton(R.string.cancel) { _, _ -> }
customize(dialogBuilder, input)
val dialog = dialogBuilder.show()
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
val okButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
// focus and show keyboard
input.requestFocus()
input.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
val text = input.text
val button = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
if (text != null && button != null) {
button.performClick()
if (text != null && okButton != null) {
okButton.performClick()
return@setOnEditorActionListener true
}
}
false
}
okButton?.setOnClickListener {
if (input.text.isBlank()) {
container.isErrorEnabled = true
container.error = ctx.getString(R.string.required)
} else {
container.isErrorEnabled = false
okAction(input.text.toString())
dialog.dismiss()
}
}
return dialog
}

View File

@@ -12,6 +12,13 @@ import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
FloatingActionButton.Behavior(context, attrs) {
var hidden: Boolean = false
companion object {
fun from(view: View): HideOnScrollFabBehavior? {
return ((view.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior as? HideOnScrollFabBehavior)
}
}
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
@@ -61,13 +68,13 @@ class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
child: FloatingActionButton,
dependency: View
): Boolean {
val behavior = BottomSheetBehaviorGoogleMapsLike.from<View>(dependency)
val behavior = BottomSheetBehaviorGoogleMapsLike.from(dependency)
when (behavior.state) {
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING -> {
}
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN -> {
child.show()
if (!hidden) child.show()
}
else -> {
child.hide()
@@ -103,7 +110,7 @@ class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
child.hide()
} else if (dyConsumed < 0 && child.visibility != View.VISIBLE) {
// User scrolled up and the FAB is currently not visible -> show the FAB
child.show()
if (!hidden) child.show()
}
}
}

View File

@@ -8,6 +8,7 @@ import jsonapi.Relationships
import jsonapi.ResourceIdentifier
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.vonforst.evmap.EspressoIdlingResource
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.model.ChargeLocation
@@ -48,6 +49,7 @@ class ChargepriceViewModel(
} else {
value = Resource.loading(null)
viewModelScope.launch {
EspressoIdlingResource.increment()
value = try {
val result = api.getVehicles()
Resource.success(result.filter {
@@ -57,6 +59,8 @@ class ChargepriceViewModel(
Resource.error(e.message, null)
} catch (e: HttpException) {
Resource.error(e.message, null)
} finally {
EspressoIdlingResource.decrement()
}
}
}
@@ -253,6 +257,7 @@ class ChargepriceViewModel(
loadPricesJob?.cancel()
loadPricesJob = viewModelScope.launch {
EspressoIdlingResource.increment()
try {
val result = api.getChargePrices(
ChargepriceRequest(
@@ -295,6 +300,8 @@ class ChargepriceViewModel(
} catch (e: HttpException) {
chargePrices.value = Resource.error(e.message, null)
chargePriceMeta.value = Resource.error(e.message, null)
} finally {
EspressoIdlingResource.decrement()
}
}
}

View File

@@ -148,13 +148,9 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
MutableLiveData<Set<Long>>()
}
val chargerSparse: MutableLiveData<ChargeLocation?> by lazy {
state.getLiveData<ChargeLocation?>("chargerSparse").apply {
observeForever {
selectedChargepoint.value = null
}
}
}
val chargerSparse: MutableLiveData<ChargeLocation?> =
state.getLiveData<ChargeLocation?>("chargerSparse")
private val triggerChargerDetailsRefresh = MutableLiveData(false)
val chargerDetails: LiveData<Resource<ChargeLocation>> = chargerSparse.switchMap { charger ->
triggerChargerDetailsRefresh.value = false
@@ -173,6 +169,12 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
val selectedChargepoint: MutableLiveData<Chargepoint?> =
state.getLiveData("selectedChargepoint")
init {
chargerSparse.observeForever {
selectedChargepoint.value = null
}
}
val charger: MediatorLiveData<Resource<ChargeLocation>> by lazy {
MediatorLiveData<Resource<ChargeLocation>>().apply {
addSource(chargerDetails) {

View File

@@ -158,7 +158,6 @@
android:textColor="@android:color/white"
app:backgroundTintAvailability="@{BindingAdaptersKt.flatten(filteredAvailability.data.status.values())}"
app:invisibleUnless="@{filteredAvailability.data != null &amp;&amp; !expanded}"
app:invisibleUnlessAnimated="@{filteredAvailability.data != null &amp;&amp; !expanded}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/txtName"
tools:backgroundTint="@color/available"

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textfield.TextInputLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>

View File

@@ -38,7 +38,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:constraint_referenced_ids="referral_tesla,referral_juicify,referral_geldfuereauto,referral_maingau,referral_ewieeinfach"
app:constraint_referenced_ids="referral_tesla,referral_juicify,referral_geldfuereauto,referral_maingau,referral_eprimo,referral_ewieeinfach"
app:flow_horizontalGap="16dp"
app:flow_horizontalStyle="packed"
app:flow_verticalAlign="baseline"
@@ -76,6 +76,13 @@
android:layout_height="wrap_content"
android:text="@string/referral_maingau" />
<Button
android:id="@+id/referral_eprimo"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/referral_eprimo" />
<Button
android:id="@+id/referral_ewieeinfach"
style="@style/Widget.Material3.Button.TonalButton"

View File

@@ -238,7 +238,8 @@
android:layout_gravity="top|end"
android:layout_marginEnd="20dp"
android:layout_marginTop="@dimen/layers_fab_top_padding"
app:tint="?android:colorControlNormal"
app:tint="?colorControlNormal"
app:backgroundTint="?android:colorBackground"
app:borderWidth="0dp"
app:srcCompat="@drawable/ic_layers"
app:layout_behavior="@string/hide_on_scroll_fab_behavior"
@@ -261,4 +262,4 @@
app:vm="@{vm}" />
</androidx.cardview.widget.CardView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
</layout>

View File

@@ -0,0 +1 @@
unqualifiedResLocale=en-US

View File

@@ -244,7 +244,7 @@
<string name="average_utilization">Průměrné využití</string>
<string name="website">Webové stránky</string>
<string name="pref_map_scale">Zobrazit ovládání přiblížení mapy</string>
<string name="pref_map_scale_meters_and_miles">Míle a metry na ovládání přiblížení mapy</string>
<string name="pref_map_scale_meters_and_miles">Míle a metry na ukazateli měřítka</string>
<string name="pref_units">Jednotky</string>
<string name="pref_units_metric">Metrické</string>
<string name="pref_units_imperial">Imperiální</string>
@@ -380,4 +380,7 @@
<string name="status_faulted">Mimo provoz</string>
<string name="status_unknown">Stav neznámý</string>
<string name="status_since">%1$s od %2$s</string>
<string name="pref_chargeprice_native_integration">Porovnání cen v EVMap</string>
<string name="pref_chargeprice_native_integration_on">Data o cenách budou zobrazena přímo v EVMap</string>
<string name="pref_chargeprice_native_integration_off">Tlačítko porovnání cen bude odkazovat na aplikaci nebo web Chargeprice</string>
</resources>

View File

@@ -375,4 +375,7 @@
<string name="status_unknown">Status unbekannt</string>
<string name="status_since">%1$s seit %2$s</string>
<string name="charger_name">Ladestationsname</string>
<string name="pref_chargeprice_native_integration">Preisvergleich in EVMap</string>
<string name="pref_chargeprice_native_integration_on">Preise werden direkt in EVMap angezeigt</string>
<string name="pref_chargeprice_native_integration_off">Preisvergleich verlinkt auf die App oder Website von Chargeprice</string>
</resources>

View File

@@ -374,4 +374,13 @@
<string name="powered_by_fronyx">previsão feita por fronyx</string>
<string name="copied">Informação copiada</string>
<string name="charger_name">Nome do carregador</string>
<string name="status_available">Disponível</string>
<string name="status_occupied">Ocupado</string>
<string name="status_charging">Carregando</string>
<string name="status_faulted">Fora de serviço</string>
<string name="status_since">%1$s desde %2$s</string>
<string name="status_unknown">Estado Desconhecido</string>
<string name="pref_chargeprice_native_integration">Comparação de preços no EVMap</string>
<string name="pref_chargeprice_native_integration_on">Os preços serão exibidos diretamente no EVMap</string>
<string name="pref_chargeprice_native_integration_off">O botão de comparação de preços abrirá a app ou site do Chargeprice</string>
</resources>

View File

@@ -40,10 +40,12 @@
<string name="geldfuereauto_referral_link" translatable="false">https://trck.geld-fuer-eauto.de/trck/eclick/c4713e9520bdb8842a3f1fbfa3a0669b3e58421043df78ad</string>
<string name="maingau_referral_link" translatable="false">https://trck.maingau-energie.de/trck/eclick/799b39cda39575dab1dcd3351abeb77b62dc33e4f9558a57</string>
<string name="ewieeinfach_referral_link" translatable="false">https://trck.e-wie-einfach.de/trck/eclick/fca74c186b54e7287a62102a13e073be4fc963825b85f7df</string>
<string name="eprimo_referral_link" translatable="false">https://netzwerk.uppr.de/trck/eclick/781768d2e779806b5e09229932662c14adddd69323594c52</string>
<string name="referral_juicify" translatable="false">Juicify</string>
<string name="referral_geldfuereauto" translatable="false">Geld für eAuto</string>
<string name="referral_maingau" translatable="false">Maingau</string>
<string name="referral_ewieeinfach" translatable="false">E wie einfach</string>
<string name="copyright_summary">©20202023 Johan von Forstner and contributors</string>
<string name="referral_eprimo" translatable="false">eprimo</string>
<string name="copyright_summary">©20202024 Johan von Forstner and contributors</string>
<string name="acra_backend_url" translatable="false">https://acra.muc.vonforst.net/report</string>
</resources>
</resources>

View File

@@ -375,4 +375,7 @@
<string name="status_unknown">Status Unknown</string>
<string name="status_since">%1$s since %2$s</string>
<string name="charger_name">Charger name</string>
<string name="pref_chargeprice_native_integration">Price comparison within EVMap</string>
<string name="pref_chargeprice_native_integration_on">Pricing data will be shown directly in EVMap</string>
<string name="pref_chargeprice_native_integration_off">Price comparison button will refer to the Chargeprice app or website</string>
</resources>

View File

@@ -15,6 +15,7 @@
<item name="preferenceTheme">@style/AppTheme.Preference</item>
<item name="alertDialogTheme">@style/AppTheme.AlertDialog</item>
<item name="materialAlertDialogTheme">@style/AppTheme.AlertDialog</item>
<item name="snackbarButtonStyle">@style/Button.TextButton.Snackbar.App</item>
</style>
<style name="AppTheme.Preference" parent="@style/PreferenceThemeOverlay">
@@ -82,6 +83,10 @@
<item name="backgroundInsetBottom">24dp</item>
</style>
<style name="Button.TextButton.Snackbar.App" parent="Widget.Material3.Button.TextButton.Snackbar">
<item name="android:textColor">@color/colorPrimary</item>
</style>
<style name="CarAppTheme">
<item name="carColorPrimary">@color/colorPrimary</item>
<item name="carColorPrimaryDark">@color/colorPrimaryDark</item>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en" />
<locale android:name="de" />
<locale android:name="fr" />
<locale android:name="nb-NO" />
</locale-config>

View File

@@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<CheckBoxPreference
android:key="chargeprice_native_integration"
android:title="@string/pref_chargeprice_native_integration"
android:summaryOn="@string/pref_chargeprice_native_integration_on"
android:summaryOff="@string/pref_chargeprice_native_integration_off"
app:defaultValue="true" />
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_vehicle"
android:title="@string/pref_my_vehicle"

View File

@@ -9,7 +9,7 @@
(e.g. in the debug version). -->
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="${applicationId}"
android:targetPackage="net.vonforst.evmap"
android:targetClass="net.vonforst.evmap.MapsActivity">
<extra
android:name="favorites"

View File

@@ -7,4 +7,10 @@ fun addDebugInterceptors(context: Context) {
}
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder = this
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder = this
object EspressoIdlingResource {
fun increment() {}
fun decrement() {}
}

View File

@@ -67,7 +67,7 @@ class NewMotionAvailabilityDetectorTest {
fun apiTest() {
for (chargepoint in listOf(2105L, 18284L)) {
val charger = runBlocking { api.getChargepointDetail(chargepoint).body()!! }
.chargelocations[0].convert("", true) as ChargeLocation
.chargelocations!![0].convert("", true) as ChargeLocation
println(charger)
runBlocking {

View File

@@ -60,7 +60,7 @@ class ChargepriceApiTest {
fun apiTest() {
for (chargepoint in listOf(2105L, 18284L)) {
val charger = runBlocking { ge.getChargepointDetail(chargepoint).body()!! }
.chargelocations[0].convert("", true) as ChargeLocation
.chargelocations!![0].convert("", true) as ChargeLocation
println(charger)
runBlocking {

View File

@@ -63,8 +63,8 @@ class GoingElectricApiTest {
val body = response.body()!!
assertEquals("ok", body.status)
assertEquals(null, body.startkey)
assertEquals(1, body.chargelocations.size)
val charger = body.chargelocations[0] as GEChargeLocation
assertEquals(1, body.chargelocations!!.size)
val charger = body.chargelocations!![0] as GEChargeLocation
assertEquals(2105, charger.id)
}
@@ -75,8 +75,8 @@ class GoingElectricApiTest {
val body = response.body()!!
assertEquals("ok", body.status)
assertEquals(null, body.startkey)
assertEquals(1, body.chargelocations.size)
val charger = body.chargelocations[0] as GEChargeLocation
assertEquals(1, body.chargelocations!!.size)
val charger = body.chargelocations!![0] as GEChargeLocation
assertEquals(34210, charger.id)
assertEquals(LocalTime.MIN, charger.openinghours!!.days!!.monday.start)
assertEquals(LocalTime.MAX, charger.openinghours!!.days!!.monday.end)
@@ -92,8 +92,8 @@ class GoingElectricApiTest {
val body = response.body()!!
assertEquals("ok", body.status)
assertEquals(null, body.startkey)
assertEquals(2, body.chargelocations.size)
val charger = body.chargelocations[0] as GEChargeLocation
assertEquals(2, body.chargelocations!!.size)
val charger = body.chargelocations!![0] as GEChargeLocation
assertEquals(41161, charger.id)
}
@@ -106,7 +106,7 @@ class GoingElectricApiTest {
val body = response.body()!!
assertEquals("ok", body.status)
assertEquals(null, body.startkey)
assertEquals(0, body.chargelocations.size)
assertEquals(0, body.chargelocations!!.size)
}
@Test
@@ -118,8 +118,8 @@ class GoingElectricApiTest {
val body = response.body()!!
assertEquals("ok", body.status)
assertEquals(2, body.startkey)
assertEquals(2, body.chargelocations.size)
val charger = body.chargelocations[0] as GEChargeLocation
assertEquals(2, body.chargelocations!!.size)
val charger = body.chargelocations!![0] as GEChargeLocation
assertEquals(41161, charger.id)
}
}

View File

@@ -14,7 +14,6 @@ buildscript {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
classpath("com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$aboutLibsVersion")
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion")
classpath("pt.jcosta.resourceplaceholders:plugin:0.7")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -27,6 +26,9 @@ allprojects {
mavenCentral()
//noinspection JcenterRepositoryObsolete
maven { setUrl("https://jitpack.io") }
maven {
setUrl("https://raw.githubusercontent.com/ev-map/mapbox-gl-native-android/mvn")
}
}
}

View File

@@ -0,0 +1,6 @@
Verbesserungen:
- Option "Sofort navigieren" wird nur angezeigt, wenn kompatible Navigationsapp installiert ist
Fehler behoben:
- OpenChargeMap: Korrektur des Verhaltens der Kartenmarker bei Filter nach Betreiber
- Abstürze behoben

View File

@@ -0,0 +1,6 @@
Improvements:
- Option "Immediate navigation" will only be shown if compatible navigation app is installed
Bugfixes:
- OpenChargeMap: Fixed strange map marker behavior when filtering by operator
- Fixed crashes