mirror of
https://github.com/ev-map/EVMap.git
synced 2026-01-03 20:47:46 -05:00
Compare commits
65 Commits
screenshot
...
car-app-li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25c5b72b22 | ||
|
|
eb74367d15 | ||
|
|
3fb290b67e | ||
|
|
398f159e27 | ||
|
|
6ab3ba2ed2 | ||
|
|
f59fd9b3aa | ||
|
|
9e18c62d9d | ||
|
|
3626c9a72f | ||
|
|
36805d8224 | ||
|
|
b1dee90068 | ||
|
|
dfc7de75ad | ||
|
|
32c7774a3a | ||
|
|
02ef25b961 | ||
|
|
e535e77b7a | ||
|
|
5b0b4e4337 | ||
|
|
a6bbf635c5 | ||
|
|
591f99dea4 | ||
|
|
0c5bd69205 | ||
|
|
72e98cf611 | ||
|
|
0fefffda2f | ||
|
|
49e555ef04 | ||
|
|
d6d1e915ee | ||
|
|
546d7a11ce | ||
|
|
4849944c23 | ||
|
|
77b38661dd | ||
|
|
3723ee161b | ||
|
|
1d3efe5295 | ||
|
|
f011944135 | ||
|
|
1d81bb5d37 | ||
|
|
e8adb759a6 | ||
|
|
f4384b4b60 | ||
|
|
1d63e37467 | ||
|
|
b5b0254bdd | ||
|
|
6514197920 | ||
|
|
4c5388350f | ||
|
|
20e9e43f0d | ||
|
|
541646dda9 | ||
|
|
b9354e77a9 | ||
|
|
65fa54ef36 | ||
|
|
6e419849b1 | ||
|
|
c4c6f09a05 | ||
|
|
3b602c03c4 | ||
|
|
012e5d7362 | ||
|
|
4f5007ca0d | ||
|
|
b40a74aa37 | ||
|
|
ce172bd4b0 | ||
|
|
b3bbca576f | ||
|
|
72ccd99c1f | ||
|
|
dd5c8659df | ||
|
|
8f7f9e5a09 | ||
|
|
8256981b8d | ||
|
|
5649ef202f | ||
|
|
ed9c729684 | ||
|
|
add66811ae | ||
|
|
2df5710910 | ||
|
|
a513a8d6d4 | ||
|
|
a55e4df62d | ||
|
|
d540faa179 | ||
|
|
3d69d3e50c | ||
|
|
fce27f0c19 | ||
|
|
da3b9643bc | ||
|
|
b690d9744d | ||
|
|
70387ec350 | ||
|
|
5a331df232 | ||
|
|
5dc5e1f43f |
2
.github/workflows/apikeys-ci.xml
vendored
2
.github/workflows/apikeys-ci.xml
vendored
@@ -1,6 +1,8 @@
|
||||
<resources>
|
||||
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">ci</string>
|
||||
<string name="mapbox_key" translatable="false">ci</string>
|
||||
<string name="jawg_key" translatable="false">ci</string>
|
||||
<string name="arcgis_key" translatable="false">ci</string>
|
||||
<string name="goingelectric_key" translatable="false">ci</string>
|
||||
<string name="chargeprice_key" translatable="false">ci</string>
|
||||
<string name="openchargemap_key" translatable="false">ci</string>
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -30,6 +30,8 @@ jobs:
|
||||
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
|
||||
CHARGEPRICE_API_KEY: ${{ secrets.CHARGEPRICE_API_KEY }}
|
||||
MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }}
|
||||
JAWG_API_KEY: ${{ secrets.JAWG_API_KEY }}
|
||||
ARCGIS_API_KEY: ${{ secrets.ARCGIS_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 }}
|
||||
|
||||
115
.github/workflows/screenshots.yml
vendored
115
.github/workflows/screenshots.yml
vendored
@@ -1,115 +0,0 @@
|
||||
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
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -34,3 +34,5 @@ jobs:
|
||||
run: ./gradlew test${{ matrix.buildvariant }}DebugUnitTest --no-daemon
|
||||
- name: Run Android Lint
|
||||
run: ./gradlew lint${{ matrix.buildvariant }}Debug --no-daemon
|
||||
- name: Check licenses
|
||||
run: ./gradlew exportLibraryDefinitions --no-daemon
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,4 +12,5 @@ apikeys.xml
|
||||
/app/**/*.apk
|
||||
/_img/connectors/*.ai
|
||||
api-7125266970515251116-798419-8e2dda660c80.json
|
||||
output-metadata.json
|
||||
output-metadata.json
|
||||
licenses_*.csv
|
||||
34
README.md
34
README.md
@@ -24,7 +24,8 @@ Features
|
||||
- Android Auto & Android Automotive OS integration
|
||||
- No ads, fully open source
|
||||
- Compatible with Android 5.0 and above
|
||||
- Can use Google Maps or Mapbox (OpenStreetMap) as map backends - the version available on F-Droid only uses Mapbox.
|
||||
- Can use Google Maps or OpenStreetMap as map backends - the version available on F-Droid only uses
|
||||
OSM.
|
||||
|
||||
Screenshots
|
||||
-----------
|
||||
@@ -42,12 +43,13 @@ EVMap uses and put them into the app in the form of a resource file called `apik
|
||||
`app/src/main/res/values`. You can find more information on which API keys are necessary for which
|
||||
features and how they can be obtained in our [documentation page](doc/api_keys.md).
|
||||
|
||||
There are three different build flavors, `googleNormal`, `fossNormal` and `googleAutomotive`.
|
||||
- The `foss` variants only use Mapbox data and should run on most Android devices, even without
|
||||
Google Play Services.
|
||||
There are four different build flavors, `googleNormal`, `fossNormal`, `googleAutomotive`, and
|
||||
`fossAutomotive`.
|
||||
|
||||
- The `foss` variants only use OSM data for the base map and place search. They should run on most Android devices, even those without Google Play Services.
|
||||
- `fossNormal` is intended to run on smartphones and tablets, and also includes the Android
|
||||
Auto app for use on the car display (however for that to work, the Android Auto app is
|
||||
necessary, which in turn does require Google Play Services).
|
||||
Auto app for use on the car display (however Android Auto may not work if the app is not
|
||||
installed from Google Play, see https://github.com/ev-map/EVMap/issues/319).
|
||||
- `fossAutomotive` can be installed directly on
|
||||
[Android Automotive OS (AAOS)](https://source.android.com/docs/automotive/start/what_automotive)
|
||||
headunits without Google services.
|
||||
@@ -75,5 +77,23 @@ You can use our [Weblate page](https://hosted.weblate.org/projects/evmap/) to he
|
||||
into new languages.
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/evmap/">
|
||||
<img src="https://hosted.weblate.org/widgets/evmap/-/open-graph.png" width="500" alt="Translation status" />
|
||||
<img src="https://hosted.weblate.org/widgets/evmap/-/open-graph.png" width="400" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
Sponsors
|
||||
--------
|
||||
|
||||
Many users currently support the development EVMap with their donations. You can find more
|
||||
information on the [Donate page](https://ev-map.app/donate/) on the EVMap website.
|
||||
|
||||
<a href="https://www.jawg.io"><img src="https://www.jawg.io/static/Blue@10x-9cdc4596e4e59acbd9ead55e9c28613e.png" alt="JawgMaps" height="58"/></a><br>
|
||||
Since May 2024, **JawgMaps** provides their OpenStreetMap vector map tiles service to EVMap for
|
||||
free, i.e. the background map displayed in the app if OpenStreetMap is selected as the data source.
|
||||
|
||||
<a href="https://chargeprice.app"><img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/powered_by_chargeprice.svg" alt="Powered by Chargeprice" height="58"/></a><br>
|
||||
Since April 2021, **Chargeprice.app** provide their price comparison API at a greatly reduced
|
||||
price for EVMap. This data is used in EVMap's price comparison feature.
|
||||
|
||||
<a href="https://fronyx.io/"><img src="https://github.com/ev-map/EVMap/blob/master/_img/powered_by_fronyx.svg" alt="Powered by Fronyx" height="68"/></a><br>
|
||||
Since September 2022, for certain charging stations, **Fronyx** provide us free access to their API
|
||||
for availability predictions.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 14 KiB |
73
_img/ic_launcher-playstore.svg
Normal file
73
_img/ic_launcher-playstore.svg
Normal file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #00e676;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2, .cls-3, .cls-4, .cls-5, .cls-6, .cls-7, .cls-8 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: rgba(255, 255, 255, .2);
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #ffb300;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #000;
|
||||
isolation: isolate;
|
||||
opacity: .45;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #90a4ae;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: #546e7a;
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: rgba(62, 39, 35, .2);
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1" data-name="Layer 1">
|
||||
<g>
|
||||
<rect class="cls-5" width="512" height="512" />
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-3"
|
||||
d="M159.42,338.98l-6.43-56.15-9.81,1.01,6.43,56.15,9.81-1.01ZM194.26,334.92l-6.43-56.15-9.81,1.01,6.43,56.15,9.81-1.01Z" />
|
||||
<path class="cls-6"
|
||||
d="M212.53,411.37c-3.04,3.72-5.41,6.09-5.75,6.43-8.79,7.1-15.9,9.13-21.65,6.43-10.15-5.07-9.47-24.02-9.13-26.05l7.1.34c-.34,5.41.68,16.91,5.41,19.28,2.71,1.35,7.44-.34,13.53-5.41h0s19.62-19.62,15.56-35.18c-4.74-18.6,16.91-45.33,24.02-54.46l1.01-1.01,5.75,4.4-1.01,1.35c-21.99,27.06-24.35,40.93-22.66,48.03,3.38,13.53-5.75,28.08-12.18,35.85Z" />
|
||||
<path class="cls-6"
|
||||
d="M137.78,338.3l2.71,23,21.31,14.21,28.75-3.04,17.59-18.6-2.71-23-67.65,7.44Z" />
|
||||
<path class="cls-7"
|
||||
d="M190.21,372.47l-28.75,3.04,6.09,25.37,22.66-2.71v-25.71h0ZM210.84,311.58l2.37,20.97-82.53,9.47-2.37-20.97,82.53-9.47Z" />
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-1"
|
||||
d="M275.45,80.22c-59.19,0-107.23,48.03-107.23,107.23,0,80.84,90.31,123.12,101.14,238.47.34,3.38,3.04,5.75,6.43,5.75s6.09-2.37,6.43-5.75c10.82-115.34,101.14-157.63,101.14-238.47-.68-59.53-48.71-107.23-107.9-107.23Z" />
|
||||
<path class="cls-2"
|
||||
d="M275.45,82.58c58.86,0,106.55,47.36,107.23,105.87v-1.01c0-59.19-48.03-107.23-107.23-107.23s-107.23,47.69-107.23,107.23v1.01c.68-58.52,48.37-105.87,107.23-105.87h0Z" />
|
||||
<path class="cls-8"
|
||||
d="M281.87,423.21c-.34,3.38-3.04,5.75-6.43,5.75s-6.09-2.37-6.43-5.75c-10.49-115.01-100.12-157.29-100.8-237.12v1.69c0,80.84,90.31,123.12,101.14,238.47.34,3.38,3.04,5.75,6.43,5.75s6.09-2.37,6.43-5.75c10.82-115.34,101.14-157.63,101.14-238.47v-1.69c-1.35,79.83-90.99,122.11-101.48,237.12h0Z" />
|
||||
</g>
|
||||
<path class="cls-4"
|
||||
d="M250.75,135.01v64.94h17.59v53.11l41.27-71.03h-23.68l23.68-47.36c.34.34-58.86.34-58.86.34Z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
23
_tools/export_licenses_faurecia.py
Normal file
23
_tools/export_licenses_faurecia.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
build_types = ["fossNormalRelease", "fossAutomotiveRelease"]
|
||||
|
||||
for build_type in build_types:
|
||||
result = subprocess.run(["gradlew.bat", f"generateLibraryDefinitions{build_type.capitalize()}"],
|
||||
capture_output=True)
|
||||
|
||||
data = json.load(
|
||||
open(f"app/build/generated/aboutLibraries/{build_type}/res/raw/aboutlibraries.json"))
|
||||
|
||||
with open(f"licenses_{build_type}.csv", "w") as f:
|
||||
f.write("component_name;license_title;license_url;public_repository;copyrights\n")
|
||||
for lib in data["libraries"]:
|
||||
license = data["licenses"][lib["licenses"][0]] if len(lib["licenses"]) > 0 else None
|
||||
license_name = license["name"] if license is not None else " "
|
||||
license_url = license["url"] if license is not None else " "
|
||||
copyrights = ", ".join([dev["name"] for dev in lib["developers"] if "name" in dev])
|
||||
if copyrights == "":
|
||||
copyrights = " "
|
||||
repo_url = lib['scm']['url'] if 'scm' in lib else ''
|
||||
f.write(f"{lib['name']};{license_name};{license_url};\"{copyrights}\";{repo_url}\n")
|
||||
@@ -12,14 +12,16 @@ plugins {
|
||||
|
||||
|
||||
android {
|
||||
useLibrary("android.car")
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "net.vonforst.evmap"
|
||||
compileSdk = 34
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode = 212
|
||||
versionName = "1.8.2"
|
||||
versionCode = 230
|
||||
versionName = "1.9.6"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -46,12 +48,21 @@ android {
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
create("releaseAutomotivePackageName") {
|
||||
// Faurecia Aptoide requires the automotive variant to use a separate package name
|
||||
initWith(getByName("release"))
|
||||
applicationIdSuffix = ".automotive"
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
isDebuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("releaseAutomotivePackageName").setRoot("src/release")
|
||||
}
|
||||
|
||||
flavorDimensions += listOf("dependencies", "automotive")
|
||||
productFlavors {
|
||||
create("foss") {
|
||||
@@ -146,6 +157,28 @@ android {
|
||||
if (mapboxKey != null) {
|
||||
resValue("string", "mapbox_key", mapboxKey)
|
||||
}
|
||||
var jawgKey =
|
||||
System.getenv("JAWG_API_KEY") ?: project.findProperty("JAWG_API_KEY")?.toString()
|
||||
if (jawgKey == null && project.hasProperty("JAWG_API_KEY_ENCRYPTED")) {
|
||||
jawgKey = decode(
|
||||
project.findProperty("JAWG_API_KEY_ENCRYPTED").toString(),
|
||||
"FmK.d,-f*p+rD+WK!eds"
|
||||
)
|
||||
}
|
||||
if (jawgKey != null) {
|
||||
resValue("string", "jawg_key", jawgKey)
|
||||
}
|
||||
var arcgisKey =
|
||||
System.getenv("ARCGIS_API_KEY") ?: project.findProperty("ARCGIS_API_KEY")?.toString()
|
||||
if (arcgisKey == null && project.hasProperty("ARCGIS_API_KEY_ENCRYPTED")) {
|
||||
arcgisKey = decode(
|
||||
project.findProperty("ARCGIS_API_KEY_ENCRYPTED").toString(),
|
||||
"FmK.d,-f*p+rD+WK!eds"
|
||||
)
|
||||
}
|
||||
if (arcgisKey != null) {
|
||||
resValue("string", "arcgis_key", jawgKey)
|
||||
}
|
||||
var chargepriceKey =
|
||||
System.getenv("CHARGEPRICE_API_KEY") ?: project.findProperty("CHARGEPRICE_API_KEY")
|
||||
?.toString()
|
||||
@@ -196,6 +229,22 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
beforeVariants { variantBuilder ->
|
||||
if (variantBuilder.buildType == "releaseAutomotivePackageName"
|
||||
&& !variantBuilder.productFlavors.containsAll(
|
||||
listOf(
|
||||
"automotive" to "automotive",
|
||||
"dependencies" to "foss"
|
||||
)
|
||||
)
|
||||
) {
|
||||
// releaseAutomotivePackageName type is only needed for fossAutomotive
|
||||
variantBuilder.enable = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
create("googleNormalImplementation") {}
|
||||
create("googleAutomotiveImplementation") {}
|
||||
@@ -203,10 +252,14 @@ configurations {
|
||||
|
||||
aboutLibraries {
|
||||
allowedLicenses = arrayOf(
|
||||
"Apache-2.0", "mit", "BSD-2-Clause",
|
||||
"Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
|
||||
"asdkl", // Android SDK
|
||||
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
|
||||
"Google Maps Platform Terms of Service" // Google Maps SDK
|
||||
"Google Maps Platform Terms of Service", // Google Maps SDK
|
||||
"provided without support or warranty", // org.json
|
||||
"Unicode/ICU License", // icu4j
|
||||
"Bouncy Castle Licence", // bcprov
|
||||
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
|
||||
)
|
||||
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
||||
}
|
||||
@@ -222,29 +275,29 @@ dependencies {
|
||||
val testGoogleImplementation by configurations
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("androidx.activity:activity-ktx:1.8.2")
|
||||
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
||||
implementation("androidx.activity:activity-ktx:1.9.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.7.1")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation("com.google.android.material:material:1.11.0")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||
implementation("androidx.browser:browser:1.7.0")
|
||||
implementation("androidx.browser:browser:1.8.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||
implementation("com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b")
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.11.0")
|
||||
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.11.0")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.12.0")
|
||||
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
|
||||
implementation("com.squareup.moshi:moshi-adapters:1.15.0")
|
||||
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
|
||||
implementation("io.coil-kt:coil:2.4.0")
|
||||
implementation("io.coil-kt:coil:2.6.0")
|
||||
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
|
||||
implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion")
|
||||
implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion")
|
||||
@@ -253,39 +306,29 @@ dependencies {
|
||||
implementation("com.google.guava:guava:29.0-android")
|
||||
implementation("com.github.pengrad:mapscaleview:1.6.0")
|
||||
implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5")
|
||||
implementation("com.github.erfansn:locale-config-x:1.0.1")
|
||||
|
||||
// Android Auto
|
||||
val carAppVersion = "1.4.0-rc02"
|
||||
val carAppVersion = "1.7.0-beta01"
|
||||
implementation("androidx.car.app:app:$carAppVersion")
|
||||
normalImplementation("androidx.car.app:app-projected:$carAppVersion")
|
||||
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
|
||||
|
||||
// AnyMaps
|
||||
val anyMapsVersion = "4854581f72"
|
||||
val anyMapsVersion = "010de4e275"
|
||||
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")
|
||||
implementation("com.github.ev-map.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")
|
||||
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-core")
|
||||
googleImplementation("com.google.android.gms:play-services-maps:19.0.0")
|
||||
implementation("com.github.ev-map.AnyMaps:anymaps-maplibre:$anyMapsVersion") {
|
||||
// duplicates classes from mapbox-sdk-services
|
||||
exclude("org.maplibre.gl", "android-sdk-geojson")
|
||||
}
|
||||
// original version of mapbox-android-core
|
||||
googleImplementation("com.mapbox.mapboxsdk:mapbox-android-core:2.0.1")
|
||||
// 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")
|
||||
}
|
||||
implementation("org.maplibre.gl:android-sdk:10.3.2-pre3") {
|
||||
exclude("org.maplibre.gl", "android-sdk-geojson")
|
||||
}
|
||||
|
||||
// Google Places
|
||||
googleImplementation("com.google.android.libraries.places:places:3.3.0")
|
||||
googleImplementation("com.google.android.libraries.places:places:3.5.0")
|
||||
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
|
||||
|
||||
// Mapbox Geocoding
|
||||
@@ -296,7 +339,7 @@ dependencies {
|
||||
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
|
||||
|
||||
// viewmodel library
|
||||
val lifecycle_version = "2.6.2"
|
||||
val lifecycle_version = "2.8.1"
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
|
||||
|
||||
@@ -305,13 +348,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.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
|
||||
implementation("com.github.anboralabs:spatia-room:0.3.0")
|
||||
|
||||
// billing library
|
||||
val billing_version = "6.1.0"
|
||||
val billing_version = "7.0.0"
|
||||
googleImplementation("com.android.billingclient:billing:$billing_version")
|
||||
googleImplementation("com.android.billingclient:billing-ktx:$billing_version")
|
||||
|
||||
@@ -325,25 +365,21 @@ 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")
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver:4.11.0")
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
|
||||
//noinspection GradleDependency
|
||||
testImplementation("org.json:json:20080701")
|
||||
testImplementation("org.robolectric:robolectric:4.11.1")
|
||||
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")
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.vonforst.evmap.storage
|
||||
package com.johan.evmap.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
* 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)
|
||||
@@ -1,155 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
171
app/src/automotive/java/net/vonforst/evmap/auto/CarInfo.kt
Normal file
171
app/src/automotive/java/net/vonforst/evmap/auto/CarInfo.kt
Normal file
@@ -0,0 +1,171 @@
|
||||
import android.car.Car
|
||||
import android.car.VehiclePropertyIds
|
||||
import android.car.VehicleUnit
|
||||
import android.car.hardware.CarPropertyValue
|
||||
import android.car.hardware.property.CarPropertyManager
|
||||
import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.annotations.ExperimentalCarApi
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.common.CarUnit
|
||||
import androidx.car.app.hardware.common.CarValue
|
||||
import androidx.car.app.hardware.common.OnCarDataAvailableListener
|
||||
import androidx.car.app.hardware.info.CarInfo
|
||||
import androidx.car.app.hardware.info.EnergyLevel
|
||||
import androidx.car.app.hardware.info.EnergyProfile
|
||||
import androidx.car.app.hardware.info.EvStatus
|
||||
import androidx.car.app.hardware.info.Mileage
|
||||
import androidx.car.app.hardware.info.Model
|
||||
import androidx.car.app.hardware.info.Speed
|
||||
import androidx.car.app.hardware.info.TollCard
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
|
||||
val CarContext.patchedCarInfo: CarInfo
|
||||
get() = CarInfoWrapper(this)
|
||||
|
||||
class CarInfoWrapper(ctx: CarContext) : CarInfo {
|
||||
private val wrapped =
|
||||
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
|
||||
private val carPropertyManager = try {
|
||||
val car = Car.createCar(ctx)
|
||||
car.getCarManager(Car.PROPERTY_SERVICE) as CarPropertyManager
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
null
|
||||
}
|
||||
private val callbacks = mutableMapOf<OnCarDataAvailableListener<*>, CarPropertyEventCallback>()
|
||||
|
||||
override fun fetchModel(executor: Executor, listener: OnCarDataAvailableListener<Model>) =
|
||||
wrapped.fetchModel(executor, listener)
|
||||
|
||||
override fun fetchEnergyProfile(
|
||||
executor: Executor,
|
||||
listener: OnCarDataAvailableListener<EnergyProfile>
|
||||
) = wrapped.fetchEnergyProfile(executor, listener)
|
||||
|
||||
override fun addTollListener(
|
||||
executor: Executor,
|
||||
listener: OnCarDataAvailableListener<TollCard>
|
||||
) = wrapped.addTollListener(executor, listener)
|
||||
|
||||
override fun removeTollListener(listener: OnCarDataAvailableListener<TollCard>) =
|
||||
wrapped.removeTollListener(listener)
|
||||
|
||||
override fun addEnergyLevelListener(
|
||||
executor: Executor,
|
||||
listener: OnCarDataAvailableListener<EnergyLevel>
|
||||
) = wrapped.addEnergyLevelListener(executor, listener)
|
||||
|
||||
override fun removeEnergyLevelListener(listener: OnCarDataAvailableListener<EnergyLevel>) =
|
||||
wrapped.removeEnergyLevelListener(listener)
|
||||
|
||||
override fun addSpeedListener(executor: Executor, listener: OnCarDataAvailableListener<Speed>) {
|
||||
// TODO: This is a emporary workaround until Car App Library 1.7.0 is released - previous versions would crash if the car reported an invalid speed display unit
|
||||
carPropertyManager ?: return
|
||||
val callback = object : CarPropertyEventCallback {
|
||||
private var speedRaw: CarPropertyValue<Float>? = null
|
||||
private var speedDisplay: CarPropertyValue<Float>? = null
|
||||
private var speedUnit: CarPropertyValue<Int>? = null
|
||||
|
||||
override fun onChangeEvent(value: CarPropertyValue<*>?) {
|
||||
when (value?.propertyId) {
|
||||
VehiclePropertyIds.PERF_VEHICLE_SPEED -> speedRaw =
|
||||
value as CarPropertyValue<Float>?
|
||||
|
||||
VehiclePropertyIds.PERF_VEHICLE_SPEED_DISPLAY -> speedDisplay =
|
||||
value as CarPropertyValue<Float>?
|
||||
|
||||
VehiclePropertyIds.VEHICLE_SPEED_DISPLAY_UNITS -> speedUnit =
|
||||
value as CarPropertyValue<Int>?
|
||||
}
|
||||
|
||||
executor.execute {
|
||||
listener.onCarDataAvailable(Speed.Builder().apply {
|
||||
speedRaw?.let {
|
||||
setRawSpeedMetersPerSecond(
|
||||
CarValue(
|
||||
it.value,
|
||||
it.timestamp,
|
||||
if (it.value != null) CarValue.STATUS_SUCCESS else CarValue.STATUS_UNKNOWN
|
||||
)
|
||||
)
|
||||
}
|
||||
speedDisplay?.let {
|
||||
setDisplaySpeedMetersPerSecond(
|
||||
CarValue(
|
||||
it.value,
|
||||
it.timestamp,
|
||||
if (it.value != null) CarValue.STATUS_SUCCESS else CarValue.STATUS_UNKNOWN
|
||||
)
|
||||
)
|
||||
}
|
||||
speedUnit?.let {
|
||||
val unit = when (it.value) {
|
||||
VehicleUnit.METER_PER_SEC -> CarUnit.METERS_PER_SEC
|
||||
VehicleUnit.MILES_PER_HOUR -> CarUnit.MILES_PER_HOUR
|
||||
VehicleUnit.KILOMETERS_PER_HOUR -> CarUnit.KILOMETERS_PER_HOUR
|
||||
else -> null
|
||||
}
|
||||
setSpeedDisplayUnit(
|
||||
CarValue(
|
||||
unit,
|
||||
it.timestamp,
|
||||
if (unit != null) CarValue.STATUS_SUCCESS else CarValue.STATUS_UNKNOWN
|
||||
)
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onErrorEvent(propertyId: Int, areaId: Int) {
|
||||
listener.onCarDataAvailable(
|
||||
Speed.Builder()
|
||||
.setRawSpeedMetersPerSecond(CarValue(null, 0, CarValue.STATUS_UNKNOWN))
|
||||
.setDisplaySpeedMetersPerSecond(CarValue(null, 0, CarValue.STATUS_UNKNOWN))
|
||||
.setSpeedDisplayUnit(CarValue(null, 0, CarValue.STATUS_UNKNOWN))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
carPropertyManager.registerCallback(
|
||||
callback,
|
||||
VehiclePropertyIds.PERF_VEHICLE_SPEED,
|
||||
CarPropertyManager.SENSOR_RATE_NORMAL
|
||||
)
|
||||
carPropertyManager.registerCallback(
|
||||
callback,
|
||||
VehiclePropertyIds.PERF_VEHICLE_SPEED_DISPLAY,
|
||||
CarPropertyManager.SENSOR_RATE_NORMAL
|
||||
)
|
||||
carPropertyManager.registerCallback(
|
||||
callback,
|
||||
VehiclePropertyIds.VEHICLE_SPEED_DISPLAY_UNITS,
|
||||
CarPropertyManager.SENSOR_RATE_NORMAL
|
||||
)
|
||||
}
|
||||
|
||||
override fun removeSpeedListener(listener: OnCarDataAvailableListener<Speed>) {
|
||||
val callback = callbacks[listener] ?: return
|
||||
carPropertyManager?.unregisterCallback(callback)
|
||||
}
|
||||
|
||||
override fun addMileageListener(
|
||||
executor: Executor,
|
||||
listener: OnCarDataAvailableListener<Mileage>
|
||||
) = wrapped.addMileageListener(executor, listener)
|
||||
|
||||
override fun removeMileageListener(listener: OnCarDataAvailableListener<Mileage>) =
|
||||
wrapped.removeMileageListener(listener)
|
||||
|
||||
@OptIn(ExperimentalCarApi::class)
|
||||
override fun addEvStatusListener(
|
||||
executor: Executor,
|
||||
listener: OnCarDataAvailableListener<EvStatus>
|
||||
) = wrapped.addEvStatusListener(executor, listener)
|
||||
|
||||
@OptIn(ExperimentalCarApi::class)
|
||||
override fun removeEvStatusListener(listener: OnCarDataAvailableListener<EvStatus>) =
|
||||
wrapped.removeEvStatusListener(listener)
|
||||
}
|
||||
@@ -1,25 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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" />
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
|
||||
@@ -2,8 +2,6 @@ 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
|
||||
@@ -36,29 +34,9 @@ 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj zasláním finančního daru vývojáři.</string>
|
||||
<string name="donate_paypal">Přispět pomocí PayPalu</string>
|
||||
<string name="data_sources_hint">Mapová data v aplikaci poskytuje služba OpenStreetMap (Mapbox).</string>
|
||||
<string name="data_sources_hint">Mapová data v aplikaci poskytuje služba OpenStreetMap.</string>
|
||||
</resources>
|
||||
@@ -2,5 +2,5 @@
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.</string>
|
||||
<string name="donate_paypal">Mit PayPal spenden</string>
|
||||
<string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap (Mapbox).</string>
|
||||
<string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap.</string>
|
||||
</resources>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur.</string>
|
||||
<string name="data_sources_hint">Les données cartographiques de l\'application sont fournies par OpenStreetMap (Mapbox).</string>
|
||||
<string name="data_sources_hint">Les données cartographiques de l\'application sont fournies par OpenStreetMap.</string>
|
||||
<string name="donate_paypal">Faire un don avec PayPal</string>
|
||||
</resources>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donate_paypal">Doner med PayPal</string>
|
||||
<string name="data_sources_hint">Kartdata i programmet tilbys av OpenStreetMap (Mapbox).</string>
|
||||
<string name="data_sources_hint">Kartdata i programmet tilbys av OpenStreetMap.</string>
|
||||
<string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende en slant til utvikleren.</string>
|
||||
</resources>
|
||||
@@ -2,5 +2,5 @@
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Vond je EVMap nuttig\? Je kan de ontwikkeling ondersteunen door een donatie te sturen naar de ontwikkelaar.</string>
|
||||
<string name="donate_paypal">Doneer via PayPal</string>
|
||||
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap (Mapbox).</string>
|
||||
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap.</string>
|
||||
</resources>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="data_sources_hint">Os dados do mapa são fornecidos pelo OpenStreetMap (Mapbox).</string>
|
||||
<string name="data_sources_hint">Os dados do mapa são fornecidos pelo OpenStreetMap.</string>
|
||||
<string name="donate_paypal">Doar com o PayPal</string>
|
||||
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.</string>
|
||||
</resources>
|
||||
@@ -2,5 +2,5 @@
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Crezi ca EVMap este util? Sprijina dezvoltarea printr-o donatie pentru dezvoltator.</string>
|
||||
<string name="donate_paypal">Doneaza cu PayPal</string>
|
||||
<string name="data_sources_hint">Hartile din aplicatie sunt furnizate de OpenStreetMap (Mapbox).</string>
|
||||
<string name="data_sources_hint">Hartile din aplicatie sunt furnizate de OpenStreetMap.</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.</string>
|
||||
<string name="donate_paypal">Donate with PayPal</string>
|
||||
<string name="data_sources_hint">Map data in the app is provided by OpenStreetMap (Mapbox).</string>
|
||||
<string name="data_sources_hint">Map data in the app is provided by OpenStreetMap.</string>
|
||||
</resources>
|
||||
@@ -3,5 +3,5 @@
|
||||
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj posláním finančního daru vývojáři.
|
||||
\n
|
||||
\nGoogle si z každého daru strhne 15 %.</string>
|
||||
<string name="data_sources_hint">V nastavení můžete také pro mapová data přepínat mezi službami Mapy Google a OpenStreetMap (Mapbox).</string>
|
||||
<string name="data_sources_hint">V nastavení můžete také pro mapová data přepínat mezi službami Mapy Google a OpenStreetMap.</string>
|
||||
</resources>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 15% Gebühren ab.</string>
|
||||
<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="data_sources_hint">In den Einstellungen kannst du auch zwischen Google Maps und OpenStreetMap für die Kartendaten wechseln.</string>
|
||||
</resources>
|
||||
@@ -3,5 +3,5 @@
|
||||
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur.
|
||||
\n
|
||||
\nGoogle prend 15% sur chaque don.</string>
|
||||
<string name="data_sources_hint">Dans les paramètres, vous pouvez également choisir entre Google Maps et OpenStreetMap (Mapbox) pour les données cartographiques.</string>
|
||||
<string name="data_sources_hint">Dans les paramètres, vous pouvez également choisir entre Google Maps et OpenStreetMap pour les données cartographiques.</string>
|
||||
</resources>
|
||||
@@ -3,5 +3,5 @@
|
||||
<string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende penger til utvikleren.
|
||||
\n
|
||||
\nGoogle tar 15% av alle donasjoner.</string>
|
||||
<string name="data_sources_hint">I innstillingene kan du også bytte mellom Google Maps og OpenStreetMap (Mapbox) for kartdata.</string>
|
||||
<string name="data_sources_hint">I innstillingene kan du også bytte mellom Google Maps og OpenStreetMap for kartdata.</string>
|
||||
</resources>
|
||||
@@ -3,5 +3,5 @@
|
||||
<string name="donations_info" formatted="false">Vind je EVMap nuttig\? Je kan de ontwikkeling steunen via een donatie aan de ontwikkelaar.
|
||||
\n
|
||||
\nGoogle houdt 15% in van elke donatie.</string>
|
||||
<string name="data_sources_hint">In de instellingen kan je ook wisselen tussen Google Maps en OpenStreetMap (Mapbox) voor de kaartgegevens.</string>
|
||||
<string name="data_sources_hint">In de instellingen kan je ook wisselen tussen Google Maps en OpenStreetMap voor de kaartgegevens.</string>
|
||||
</resources>
|
||||
@@ -3,5 +3,5 @@
|
||||
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.
|
||||
\n
|
||||
\nA Google cobra 15% de cada doação.</string>
|
||||
<string name="data_sources_hint">Também pode mudar entre o Google Maps e OpenStreetMap (Mapbox) nas definições da app.</string>
|
||||
<string name="data_sources_hint">Também pode mudar entre o Google Maps e OpenStreetMap nas definições da app.</string>
|
||||
</resources>
|
||||
@@ -2,7 +2,7 @@
|
||||
<resources>
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>@string/pref_provider_google_maps</item>
|
||||
<item>@string/pref_provider_osm_mapbox</item>
|
||||
<item>@string/pref_provider_osm</item>
|
||||
</string-array>
|
||||
<string-array name="pref_map_provider_values" translatable="false">
|
||||
<item>google</item>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 15% off every donation.</string>
|
||||
<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="data_sources_hint">In the settings you can also switch between Google Maps and OpenStreetMap for the map data.</string>
|
||||
</resources>
|
||||
@@ -9,6 +9,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
|
||||
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
|
||||
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
|
||||
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />
|
||||
|
||||
@@ -47,6 +48,14 @@
|
||||
android:name="com.mapbox.ACCESS_TOKEN"
|
||||
android:value="@string/mapbox_key" />
|
||||
|
||||
<meta-data
|
||||
android:name="io.jawg.ACCESS_TOKEN"
|
||||
android:value="@string/jawg_key" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.arcgis.ACCESS_TOKEN"
|
||||
android:value="@string/arcgis_key" />
|
||||
|
||||
<activity
|
||||
android:name=".MapsActivity"
|
||||
android:label="@string/app_name"
|
||||
@@ -339,9 +348,8 @@
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="location">
|
||||
<intent-filter>
|
||||
<action
|
||||
android:name="androidx.car.app.CarAppService"
|
||||
android:category="androidx.car.app.category.POI" />
|
||||
<action android:name="androidx.car.app.CarAppService" />
|
||||
<category android:name="androidx.car.app.category.POI" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -3,7 +3,11 @@ package net.vonforst.evmap
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import androidx.work.*
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import net.vonforst.evmap.storage.CleanupCacheWorker
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.updateAppLocale
|
||||
@@ -24,7 +28,7 @@ class EvMapApplication : Application(), Configuration.Provider {
|
||||
|
||||
// Convert to new AppCompat storage for app language
|
||||
val lang = prefs.language
|
||||
if (lang != null && lang !in listOf("", "default")) {
|
||||
if (lang != null) {
|
||||
updateAppLocale(lang)
|
||||
prefs.language = null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.app.ActivityOptions
|
||||
import android.app.PendingIntent
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
@@ -61,6 +60,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
|
||||
setContentView(R.layout.activity_maps)
|
||||
|
||||
val drawerLayout = findViewById<DrawerLayout>(R.id.drawer_layout)
|
||||
appBarConfiguration = AppBarConfiguration(
|
||||
setOf(
|
||||
R.id.map,
|
||||
@@ -68,7 +68,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
R.id.about,
|
||||
R.id.settings
|
||||
),
|
||||
findViewById<DrawerLayout>(R.id.drawer_layout)
|
||||
drawerLayout
|
||||
)
|
||||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||
@@ -88,6 +88,17 @@ class MapsActivity : AppCompatActivity(),
|
||||
|
||||
checkPlayServices(this)
|
||||
|
||||
navController.setGraph(navGraph, MapFragmentArgs(appStart = true).toBundle())
|
||||
var deepLink: PendingIntent? = null
|
||||
|
||||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||
if (destination.id == R.id.onboarding) {
|
||||
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
} else {
|
||||
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
|
||||
}
|
||||
}
|
||||
|
||||
if (!prefs.welcomeDialogShown || !prefs.dataSourceSet) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// wait for splash screen animation to finish on first start
|
||||
@@ -105,110 +116,104 @@ class MapsActivity : AppCompatActivity(),
|
||||
}
|
||||
})
|
||||
}
|
||||
navGraph.setStartDestination(R.id.onboarding)
|
||||
navController.graph = navGraph
|
||||
return
|
||||
} else if (!prefs.privacyAccepted) {
|
||||
navGraph.setStartDestination(R.id.onboarding)
|
||||
navController.graph = navGraph
|
||||
return
|
||||
} else {
|
||||
navGraph.setStartDestination(R.id.map)
|
||||
navController.setGraph(navGraph, MapFragmentArgs(appStart = true).toBundle())
|
||||
var deepLink: PendingIntent? = null
|
||||
} else if (intent?.scheme == "geo") {
|
||||
val query = intent.data?.query?.split("=")?.get(1)
|
||||
val coords = getLocationFromIntent(intent)
|
||||
|
||||
if (intent?.scheme == "geo") {
|
||||
val query = intent.data?.query?.split("=")?.get(1)
|
||||
val coords = getLocationFromIntent(intent)
|
||||
|
||||
if (coords != null) {
|
||||
val lat = coords[0]
|
||||
val lon = coords[1]
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(latLng = LatLng(lat, lon)).toBundle())
|
||||
.createPendingIntent()
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(locationName = query).toBundle())
|
||||
.createPendingIntent()
|
||||
if (coords != null) {
|
||||
val lat = coords[0]
|
||||
val lon = coords[1]
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(latLng = LatLng(lat, lon)).toBundle())
|
||||
.createPendingIntent()
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(locationName = query).toBundle())
|
||||
.createPendingIntent()
|
||||
}
|
||||
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
|
||||
val id = intent.data?.pathSegments?.lastOrNull()?.toLongOrNull()
|
||||
if (id != null) {
|
||||
if (prefs.dataSource != "goingelectric") {
|
||||
prefs.dataSource = "goingelectric"
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(
|
||||
R.string.data_source_switched_to,
|
||||
getString(R.string.data_source_goingelectric)
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
|
||||
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
|
||||
if (id != null) {
|
||||
if (prefs.dataSource != "goingelectric") {
|
||||
prefs.dataSource = "goingelectric"
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(
|
||||
R.string.data_source_switched_to,
|
||||
getString(R.string.data_source_goingelectric)
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
|
||||
.createPendingIntent()
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
|
||||
.createPendingIntent()
|
||||
}
|
||||
} else if (intent?.scheme == "https" && intent?.data?.host in listOf(
|
||||
"openchargemap.org",
|
||||
"map.openchargemap.io"
|
||||
)
|
||||
) {
|
||||
val id = when (intent.data?.host) {
|
||||
"openchargemap.org" -> intent.data?.pathSegments?.lastOrNull()?.toLongOrNull()
|
||||
"map.openchargemap.io" -> intent.data?.getQueryParameter("id")?.toLongOrNull()
|
||||
else -> null
|
||||
}
|
||||
if (id != null) {
|
||||
if (prefs.dataSource != "openchargemap") {
|
||||
prefs.dataSource = "openchargemap"
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(
|
||||
R.string.data_source_switched_to,
|
||||
getString(R.string.data_source_openchargemap)
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
} else if (intent?.scheme == "https" && intent?.data?.host in listOf("openchargemap.org", "map.openchargemap.io")) {
|
||||
val id = when (intent.data?.host) {
|
||||
"openchargemap.org" -> intent.data?.pathSegments?.last()?.toLongOrNull()
|
||||
"map.openchargemap.io" -> intent.data?.getQueryParameter("id")?.toLongOrNull()
|
||||
else -> null
|
||||
}
|
||||
if (id != null) {
|
||||
if (prefs.dataSource != "openchargemap") {
|
||||
prefs.dataSource = "openchargemap"
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(
|
||||
R.string.data_source_switched_to,
|
||||
getString(R.string.data_source_openchargemap)
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
|
||||
.createPendingIntent()
|
||||
}
|
||||
} else if (intent.scheme == "net.vonforst.evmap") {
|
||||
intent.data?.let {
|
||||
if (it.host == "find_charger") {
|
||||
val lat = it.getQueryParameter("latitude")?.toDouble()
|
||||
val lon = it.getQueryParameter("longitude")?.toDouble()
|
||||
val name = it.getQueryParameter("name")
|
||||
if (lat != null && lon != null) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(
|
||||
MapFragmentArgs(
|
||||
latLng = LatLng(lat, lon),
|
||||
locationName = name
|
||||
).toBundle()
|
||||
)
|
||||
.createPendingIntent()
|
||||
} else if (name != null) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(locationName = name).toBundle())
|
||||
.createPendingIntent()
|
||||
}
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
|
||||
.createPendingIntent()
|
||||
}
|
||||
} else if (intent.scheme == "net.vonforst.evmap") {
|
||||
intent.data?.let {
|
||||
if (it.host == "find_charger") {
|
||||
val lat = it.getQueryParameter("latitude")?.toDouble()
|
||||
val lon = it.getQueryParameter("longitude")?.toDouble()
|
||||
val name = it.getQueryParameter("name")
|
||||
if (lat != null && lon != null) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(
|
||||
MapFragmentArgs(
|
||||
latLng = LatLng(lat, lon),
|
||||
locationName = name
|
||||
).toBundle()
|
||||
)
|
||||
.createPendingIntent()
|
||||
} else if (name != null) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(locationName = name).toBundle())
|
||||
.createPendingIntent()
|
||||
}
|
||||
}
|
||||
} else if (intent.hasExtra(EXTRA_CHARGER_ID)) {
|
||||
navController.navigate(
|
||||
R.id.map, MapFragmentArgs(
|
||||
}
|
||||
} 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),
|
||||
@@ -216,20 +221,20 @@ class MapsActivity : AppCompatActivity(),
|
||||
)
|
||||
).toBundle()
|
||||
)
|
||||
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(navGraph)
|
||||
.setDestination(R.id.favs)
|
||||
.createPendingIntent()
|
||||
} else if (intent.hasExtra(EXTRA_DONATE)) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(navGraph)
|
||||
.setDestination(R.id.donate)
|
||||
.createPendingIntent()
|
||||
}
|
||||
|
||||
deepLink?.send()
|
||||
.createPendingIntent()
|
||||
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(navGraph)
|
||||
.setDestination(R.id.favs)
|
||||
.createPendingIntent()
|
||||
} else if (intent.hasExtra(EXTRA_DONATE)) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(navGraph)
|
||||
.setDestination(R.id.donate)
|
||||
.createPendingIntent()
|
||||
}
|
||||
|
||||
deepLink?.send()
|
||||
}
|
||||
|
||||
fun navigateTo(charger: ChargeLocation) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import android.text.SpannedString
|
||||
import android.text.TextUtils
|
||||
import android.text.style.StyleSpan
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
fun Bundle.optDouble(name: String): Double? {
|
||||
@@ -139,4 +140,6 @@ fun PackageManager.isAppInstalled(packageName: String): Boolean {
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun currencyDisplayName(code: String) = "${Currency.getInstance(code).displayName} ($code)"
|
||||
@@ -9,7 +9,6 @@ import androidx.core.text.buildSpannedString
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.tesla.Pricing
|
||||
import net.vonforst.evmap.api.availability.tesla.Rates
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaChargingOwnershipGraphQlApi
|
||||
import net.vonforst.evmap.bold
|
||||
import net.vonforst.evmap.joinToSpannedString
|
||||
import net.vonforst.evmap.model.ChargeCard
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.memory.MemoryCache
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
|
||||
@@ -71,7 +70,7 @@ class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener?
|
||||
memoryKeys[item.id] = metadata.memoryCacheKey
|
||||
}
|
||||
)
|
||||
allowHardware(!BuildConfig.DEBUG)
|
||||
allowHardware(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.api.RateLimitInterceptor
|
||||
import net.vonforst.evmap.api.await
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.cartesianProduct
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
@@ -18,7 +16,6 @@ import net.vonforst.evmap.viewmodel.Resource
|
||||
import okhttp3.Cache
|
||||
import okhttp3.JavaNetCookieJar
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.net.CookieManager
|
||||
@@ -41,16 +38,6 @@ interface AvailabilityDetector {
|
||||
abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : AvailabilityDetector {
|
||||
protected val radius = 150 // max radius in meters
|
||||
|
||||
protected suspend fun httpGet(url: String): String {
|
||||
val request = Request.Builder().url(url).build()
|
||||
val response = client.newCall(request).await()
|
||||
|
||||
if (!response.isSuccessful) throw IOException(response.message)
|
||||
|
||||
val str = response.body!!.string()
|
||||
return str
|
||||
}
|
||||
|
||||
protected fun getCorrespondingChargepoint(
|
||||
cps: Iterable<Chargepoint>, type: String, power: Double
|
||||
): Chargepoint? {
|
||||
|
||||
@@ -52,32 +52,31 @@ class TeslaGuestAvailabilityDetector(
|
||||
|
||||
val (detailsA, guestPricing) = coroutineScope {
|
||||
val details = async {
|
||||
api.getChargingSiteDetails(
|
||||
TeslaChargingGuestGraphQlApi.GetChargingSiteDetailsRequest(
|
||||
TeslaChargingGuestGraphQlApi.GetChargingSiteInformationVariables(
|
||||
api.getSiteDetails(
|
||||
TeslaChargingGuestGraphQlApi.GetSiteDetailsRequest(
|
||||
TeslaChargingGuestGraphQlApi.GetSiteDetailsVariables(
|
||||
TeslaChargingGuestGraphQlApi.Identifier(
|
||||
TeslaChargingGuestGraphQlApi.ChargingSiteIdentifier(
|
||||
trtId
|
||||
trtId, TeslaChargingGuestGraphQlApi.Experience.ADHOC
|
||||
)
|
||||
),
|
||||
TeslaChargingGuestGraphQlApi.Experience.ADHOC
|
||||
)
|
||||
)
|
||||
).data.site ?: throw AvailabilityDetectorException("no candidates found.")
|
||||
).data.chargingNetwork?.site
|
||||
?: throw AvailabilityDetectorException("no candidates found.")
|
||||
}
|
||||
val guestPricing = async {
|
||||
api.getChargingSiteDetails(
|
||||
TeslaChargingGuestGraphQlApi.GetChargingSiteDetailsRequest(
|
||||
TeslaChargingGuestGraphQlApi.GetChargingSiteInformationVariables(
|
||||
api.getSiteDetails(
|
||||
TeslaChargingGuestGraphQlApi.GetSiteDetailsRequest(
|
||||
TeslaChargingGuestGraphQlApi.GetSiteDetailsVariables(
|
||||
TeslaChargingGuestGraphQlApi.Identifier(
|
||||
TeslaChargingGuestGraphQlApi.ChargingSiteIdentifier(
|
||||
trtId
|
||||
trtId, TeslaChargingGuestGraphQlApi.Experience.GUEST
|
||||
)
|
||||
),
|
||||
TeslaChargingGuestGraphQlApi.Experience.GUEST
|
||||
)
|
||||
)
|
||||
).data.site?.pricing
|
||||
).data.chargingNetwork?.site?.pricing
|
||||
}
|
||||
details to guestPricing
|
||||
}
|
||||
@@ -103,12 +102,9 @@ class TeslaGuestAvailabilityDetector(
|
||||
"charger has unknown connectors"
|
||||
)
|
||||
|
||||
val chargerDetails = details.chargersAvailable.chargerDetails
|
||||
val chargers = details.chargers.associateBy { it.id }
|
||||
var detailsSorted = chargerDetails
|
||||
.sortedBy { chargers[it.id]?.labelLetter }
|
||||
.sortedBy { chargers[it.id]?.labelNumber }
|
||||
|
||||
var detailsSorted = details.chargerList
|
||||
.sortedBy { c -> c.labelLetter }
|
||||
.sortedBy { c -> c.labelNumber }
|
||||
|
||||
if (detailsSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) {
|
||||
// apparently some connectors are missing in Tesla data
|
||||
@@ -120,7 +116,7 @@ class TeslaGuestAvailabilityDetector(
|
||||
detailsSorted + List(numMissing) {
|
||||
TeslaChargingGuestGraphQlApi.ChargerDetail(
|
||||
ChargerAvailability.UNKNOWN,
|
||||
""
|
||||
"", ""
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -151,7 +147,7 @@ class TeslaGuestAvailabilityDetector(
|
||||
}
|
||||
|
||||
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
|
||||
val labelsMap = detailsMap.mapValues { it.value.map { chargers[it.id]?.label } }
|
||||
val labelsMap = detailsMap.mapValues { it.value.map { it.label } }
|
||||
|
||||
val pricing = details.pricing.copy(memberRates = guestPricing.await()?.userRates)
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package net.vonforst.evmap.api.availability.tesla
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
@@ -15,7 +11,6 @@ import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
import java.lang.reflect.Type
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface TeslaCuaApi {
|
||||
@@ -71,24 +66,22 @@ interface TeslaCuaApi {
|
||||
|
||||
interface TeslaChargingGuestGraphQlApi {
|
||||
@POST("graphql")
|
||||
suspend fun getChargingSiteDetails(
|
||||
@Body request: GetChargingSiteDetailsRequest,
|
||||
@Query("operationName") operationName: String = "getGuestChargingSiteDetails"
|
||||
suspend fun getSiteDetails(
|
||||
@Body request: GetSiteDetailsRequest,
|
||||
@Query("operationName") operationName: String = "GetSiteDetails"
|
||||
): GetChargingSiteDetailsResponse
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteDetailsRequest(
|
||||
override val variables: GetChargingSiteInformationVariables,
|
||||
override val operationName: String = "getGuestChargingSiteDetails",
|
||||
data class GetSiteDetailsRequest(
|
||||
override val variables: GetSiteDetailsVariables,
|
||||
override val operationName: String = "GetSiteDetails",
|
||||
override val query: String =
|
||||
"\n query getGuestChargingSiteDetails(\$identifier: ChargingSiteIdentifierInput!, \$deviceLocale: String!, \$experience: ChargingExperienceEnum!) {\n site(\n identifier: \$identifier\n deviceLocale: \$deviceLocale\n experience: \$experience\n ) {\n activeOutages\n address {\n countryCode\n }\n chargers {\n id\n label\n }\n chargersAvailable {\n chargerDetails {\n id\n availability\n }\n }\n holdAmount {\n holdAmount\n currencyCode\n }\n maxPowerKw\n name\n programType\n publicStallCount\n id\n pricing(experience: \$experience) {\n userRates {\n activePricebook {\n charging {\n uom\n rates\n buckets {\n start\n end\n }\n bucketUom\n currencyCode\n programType\n vehicleMakeType\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n }\n parking {\n uom\n rates\n buckets {\n start\n end\n }\n bucketUom\n currencyCode\n programType\n vehicleMakeType\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n }\n congestion {\n uom\n rates\n buckets {\n start\n end\n }\n bucketUom\n currencyCode\n programType\n vehicleMakeType\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n }\n }\n }\n }\n }\n}\n "
|
||||
"\n query GetSiteDetails(\$siteId: SiteIdInput!) {\n chargingNetwork {\n site(siteId: \$siteId) {\n address {\n countryCode\n }\n chargerList {\n id\n label\n availability\n }\n holdAmount {\n amount\n currencyCode\n }\n maxPowerKw\n name\n programType\n publicStallCount\n trtId\n pricing {\n userRates {\n activePricebook {\n charging {\n ...ChargingRate\n }\n parking {\n ...ChargingRate\n }\n congestion {\n ...ChargingRate\n }\n }\n }\n }\n }\n }\n}\n \n fragment ChargingRate on ChargingUserRate {\n uom\n rates\n buckets {\n start\n end\n }\n bucketUom\n currencyCode\n programType\n vehicleMakeType\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n}\n "
|
||||
) : GraphQlRequest()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteInformationVariables(
|
||||
val identifier: Identifier,
|
||||
val experience: Experience,
|
||||
val deviceLocale: String = "de-DE",
|
||||
data class GetSiteDetailsVariables(
|
||||
val siteId: Identifier,
|
||||
)
|
||||
|
||||
enum class Experience {
|
||||
@@ -97,22 +90,22 @@ interface TeslaChargingGuestGraphQlApi {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Identifier(
|
||||
val siteId: ChargingSiteIdentifier
|
||||
val byTrtId: ChargingSiteIdentifier
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargingSiteIdentifier(
|
||||
val id: Long,
|
||||
val siteType: SiteType = SiteType.SUPERCHARGER
|
||||
val trtId: Long,
|
||||
val chargingExperience: Experience,
|
||||
val programType: String = "PTSCH",
|
||||
val locale: String = "de-DE",
|
||||
)
|
||||
|
||||
enum class SiteType {
|
||||
@Json(name = "SITE_TYPE_SUPERCHARGER")
|
||||
SUPERCHARGER
|
||||
}
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteDetailsResponse(val data: GetChargingSiteDetailsResponseDataNetwork)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteDetailsResponse(val data: GetChargingSiteDetailsResponseData)
|
||||
data class GetChargingSiteDetailsResponseDataNetwork(val chargingNetwork: GetChargingSiteDetailsResponseData?)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteDetailsResponseData(val site: ChargingSiteInformation?)
|
||||
@@ -120,9 +113,8 @@ interface TeslaChargingGuestGraphQlApi {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargingSiteInformation(
|
||||
val activeOutages: List<Outage>?,
|
||||
val chargers: List<ChargerId>,
|
||||
val chargersAvailable: ChargersAvailable,
|
||||
val id: Long,
|
||||
val chargerList: List<ChargerDetail>,
|
||||
val trtId: Long,
|
||||
val maxPowerKw: Int,
|
||||
val name: String,
|
||||
val pricing: Pricing,
|
||||
@@ -130,9 +122,10 @@ interface TeslaChargingGuestGraphQlApi {
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargerId(
|
||||
val id: String,
|
||||
data class ChargerDetail(
|
||||
val availability: ChargerAvailability,
|
||||
val label: String?,
|
||||
val id: String
|
||||
) {
|
||||
val labelNumber
|
||||
get() = label?.replace(Regex("""\D"""), "")?.toInt()
|
||||
@@ -140,15 +133,6 @@ interface TeslaChargingGuestGraphQlApi {
|
||||
get() = label?.replace(Regex("""\d"""), "")
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargersAvailable(val chargerDetails: List<ChargerDetail>)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargerDetail(
|
||||
val availability: ChargerAvailability,
|
||||
val id: String
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun create(
|
||||
client: OkHttpClient,
|
||||
|
||||
@@ -452,7 +452,10 @@ class GoingElectricApiWrapper(
|
||||
if (responses.map { it.isSuccessful }.all { it }
|
||||
&& plugsResponse.body()!!.status == STATUS_OK
|
||||
&& chargeCardsResponse.body()!!.status == STATUS_OK
|
||||
&& networksResponse.body()!!.status == STATUS_OK) {
|
||||
&& networksResponse.body()!!.status == STATUS_OK
|
||||
&& plugsResponse.body()!!.result != null
|
||||
&& chargeCardsResponse.body()!!.result != null
|
||||
&& networksResponse.body()!!.result != null) {
|
||||
Resource.success(
|
||||
GEReferenceData(
|
||||
plugsResponse.body()!!.result!!,
|
||||
|
||||
@@ -244,6 +244,8 @@ class OpenChargeMapApiWrapper(
|
||||
return Resource.success(ChargepointList(result, data.size < 499))
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ data class OCMUserComment(
|
||||
@Json(name = "ID") val id: Long,
|
||||
@Json(name = "CommentTypeID") val commentTypeId: Long,
|
||||
@Json(name = "Comment") val comment: String?,
|
||||
@Json(name = "UserName") val userName: String,
|
||||
@Json(name = "UserName") val userName: String?,
|
||||
@Json(name = "DateCreated") val dateCreated: ZonedDateTime
|
||||
)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.car2go.maps.model.LatLng
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.location.FusionEngine
|
||||
import net.vonforst.evmap.location.LocationEngine
|
||||
import net.vonforst.evmap.location.Priority
|
||||
@@ -125,8 +126,11 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
||||
}
|
||||
|
||||
override fun onCreateScreen(intent: Intent): Screen {
|
||||
|
||||
val mapScreen = MapScreen(carContext, this)
|
||||
val mapScreen = if (supportsNewMapScreen(carContext)) {
|
||||
MapScreen(carContext, this)
|
||||
} else {
|
||||
LegacyMapScreen(carContext, this)
|
||||
}
|
||||
val screens = mutableListOf<Screen>(mapScreen)
|
||||
|
||||
handleActionsIntent(intent)?.let {
|
||||
@@ -186,7 +190,7 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
||||
val lon = it.getQueryParameter("longitude")?.toDouble()
|
||||
val name = it.getQueryParameter("name")
|
||||
if (lat != null && lon != null) {
|
||||
prefs.placeSearchResultAndroidAuto = LatLng(lat, lon)
|
||||
prefs.placeSearchResultAndroidAuto = PlaceWithBounds(LatLng(lat, lon), null)
|
||||
prefs.placeSearchResultAndroidAutoName = name ?: "%.4f,%.4f".format(lat, lon)
|
||||
return null
|
||||
} else if (name != null) {
|
||||
|
||||
@@ -3,7 +3,6 @@ package net.vonforst.evmap.auto
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
@@ -52,9 +51,6 @@ import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.fronyx.FronyxApi
|
||||
import net.vonforst.evmap.api.fronyx.PredictionData
|
||||
import net.vonforst.evmap.api.fronyx.PredictionRepository
|
||||
import net.vonforst.evmap.api.iconForPlugType
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Cost
|
||||
import net.vonforst.evmap.model.FaultReport
|
||||
@@ -64,7 +60,6 @@ import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.availabilityText
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.awaitFinished
|
||||
@@ -74,7 +69,6 @@ import java.time.format.FormatStyle
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Screen(ctx) {
|
||||
var charger: ChargeLocation? = null
|
||||
var photo: Bitmap? = null
|
||||
@@ -134,7 +128,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
.setFlags(Action.FLAG_PRIMARY)
|
||||
.setBackgroundColor(CarColor.PRIMARY)
|
||||
.setOnClickListener {
|
||||
navigateToCharger(charger)
|
||||
navigateToCharger(carContext, charger)
|
||||
}
|
||||
.build())
|
||||
if (ChargepriceApi.isChargerSupported(charger)) {
|
||||
@@ -271,7 +265,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
Row.IMAGE_TYPE_LARGE
|
||||
)
|
||||
}
|
||||
addText(generateChargepointsText(charger))
|
||||
addText(generateChargepointsText(charger, availability, carContext))
|
||||
}.build())
|
||||
if (maxRows <= 3) {
|
||||
// row 2: operator + cost + fault report
|
||||
@@ -484,47 +478,6 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
return string
|
||||
}
|
||||
|
||||
private fun generateChargepointsText(charger: ChargeLocation): SpannableStringBuilder {
|
||||
val chargepointsText = SpannableStringBuilder()
|
||||
charger.chargepointsMerged.forEachIndexed { i, cp ->
|
||||
chargepointsText.apply {
|
||||
if (i > 0) append(" · ")
|
||||
append("${cp.count}× ")
|
||||
val plugIcon = iconForPlugType(cp.type)
|
||||
if (plugIcon != 0) {
|
||||
append(
|
||||
nameForPlugType(carContext.stringProvider(), cp.type),
|
||||
CarIconSpan.create(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
plugIcon
|
||||
)
|
||||
).setTint(
|
||||
CarColor.createCustom(Color.WHITE, Color.BLACK)
|
||||
).build()
|
||||
),
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
} else {
|
||||
append(nameForPlugType(carContext.stringProvider(), cp.type))
|
||||
}
|
||||
cp.formatPower()?.let {
|
||||
append(" ")
|
||||
append(it)
|
||||
}
|
||||
}
|
||||
availability?.status?.get(cp)?.let { status ->
|
||||
chargepointsText.append(
|
||||
" (${availabilityText(status)}/${cp.count})",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
return chargepointsText
|
||||
}
|
||||
|
||||
private fun generateOperatorText(charger: ChargeLocation) =
|
||||
if (charger.operator != null && charger.network != null) {
|
||||
if (charger.operator.contains(charger.network)) {
|
||||
@@ -542,16 +495,6 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
carContext.getString(R.string.unknown_operator)
|
||||
}
|
||||
|
||||
private fun navigateToCharger(charger: ChargeLocation) {
|
||||
val coord = charger.coordinates
|
||||
val intent =
|
||||
Intent(
|
||||
CarContext.ACTION_NAVIGATE,
|
||||
Uri.parse("geo:${coord.lat},${coord.lng}")
|
||||
)
|
||||
carContext.startCarApp(intent)
|
||||
}
|
||||
|
||||
private fun loadCharger() {
|
||||
lifecycleScope.launch {
|
||||
favorite = db.favoritesDao().findFavorite(chargerSparse.id, chargerSparse.dataSource)
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.location.Location
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.hardware.info.EnergyLevel
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.CarIconSpan
|
||||
import androidx.car.app.model.CarLocation
|
||||
import androidx.car.app.model.CarText
|
||||
import androidx.car.app.model.DistanceSpan
|
||||
import androidx.car.app.model.ForegroundCarColorSpan
|
||||
import androidx.car.app.model.ItemList
|
||||
import androidx.car.app.model.Metadata
|
||||
import androidx.car.app.model.Pane
|
||||
import androidx.car.app.model.Place
|
||||
import androidx.car.app.model.PlaceMarker
|
||||
import androidx.car.app.model.Row
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.FILTERS_FAVORITES
|
||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.availabilityText
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
interface ChargerListDelegate : ItemList.OnItemVisibilityChangedListener {
|
||||
val locationError: Boolean
|
||||
val loadingError: Boolean
|
||||
val maxRows: Int
|
||||
val filterStatus: Long
|
||||
val location: Location?
|
||||
val energyLevel: EnergyLevel?
|
||||
fun onChargerClick(charger: ChargeLocation)
|
||||
}
|
||||
|
||||
class ChargerListFormatter(val carContext: CarContext, val screen: ChargerListDelegate) {
|
||||
private val iconGen = ChargerIconGenerator(carContext, null, height = 96)
|
||||
var favorites: Set<Long> = emptySet()
|
||||
|
||||
fun buildChargerList(
|
||||
chargers: List<ChargeLocation>?,
|
||||
availabilities: Map<Long, Pair<ZonedDateTime, ChargeLocationStatus?>>
|
||||
): ItemList? {
|
||||
return if (chargers != null) {
|
||||
val chargerList = chargers.take(screen.maxRows)
|
||||
val builder = ItemList.Builder()
|
||||
// only show the city if not all chargers are in the same city
|
||||
val showCity = chargerList.map { it.address?.city }.distinct().size > 1
|
||||
chargerList.forEach { charger ->
|
||||
builder.addItem(
|
||||
formatCharger(
|
||||
charger,
|
||||
availabilities,
|
||||
showCity,
|
||||
charger.id in favorites
|
||||
)
|
||||
)
|
||||
}
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(
|
||||
if (screen.filterStatus == FILTERS_FAVORITES) {
|
||||
R.string.auto_no_favorites_found
|
||||
} else {
|
||||
R.string.auto_no_chargers_found
|
||||
}
|
||||
)
|
||||
)
|
||||
builder.setOnItemsVisibilityChangedListener(screen)
|
||||
builder.build()
|
||||
} else {
|
||||
if (screen.loadingError) {
|
||||
val builder = ItemList.Builder()
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(R.string.connection_error)
|
||||
)
|
||||
builder.build()
|
||||
} else if (screen.locationError) {
|
||||
val builder = ItemList.Builder()
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(R.string.location_error)
|
||||
)
|
||||
builder.build()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatCharger(
|
||||
charger: ChargeLocation,
|
||||
availabilities: Map<Long, Pair<ZonedDateTime, ChargeLocationStatus?>>,
|
||||
showCity: Boolean,
|
||||
isFavorite: Boolean
|
||||
): Row {
|
||||
val markerTint = getMarkerTint(charger)
|
||||
val backgroundTint = if ((charger.maxPower ?: 0.0) > 100) {
|
||||
R.color.charger_100kw_dark // slightly darker color for better contrast
|
||||
} else {
|
||||
markerTint
|
||||
}
|
||||
val color = ContextCompat.getColor(carContext, backgroundTint)
|
||||
val place =
|
||||
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
|
||||
.setMarker(
|
||||
PlaceMarker.Builder()
|
||||
.setColor(CarColor.createCustom(color, color))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
val icon = iconGen.getBitmap(
|
||||
markerTint,
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti(),
|
||||
fav = isFavorite
|
||||
)
|
||||
val iconSpan =
|
||||
CarIconSpan.create(CarIcon.Builder(IconCompat.createWithBitmap(icon)).build())
|
||||
|
||||
return Row.Builder().apply {
|
||||
// only show the city if not all chargers are in the same city (-> showCity == true)
|
||||
// and the city is not already contained in the charger name
|
||||
val title = SpannableStringBuilder().apply {
|
||||
append(" ", iconSpan, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
append(" ")
|
||||
append(charger.name)
|
||||
}
|
||||
if (showCity && charger.address?.city != null && charger.address.city !in charger.name) {
|
||||
val titleWithCity = SpannableStringBuilder().apply {
|
||||
append("", iconSpan, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
append(" ")
|
||||
append("${charger.name} · ${charger.address.city}")
|
||||
}
|
||||
setTitle(CarText.Builder(titleWithCity).addVariant(title).build())
|
||||
} else {
|
||||
setTitle(title)
|
||||
}
|
||||
|
||||
val text = SpannableStringBuilder()
|
||||
|
||||
// distance
|
||||
screen.location?.let {
|
||||
val distanceMeters = distanceBetween(
|
||||
it.latitude, it.longitude,
|
||||
charger.coordinates.lat, charger.coordinates.lng
|
||||
)
|
||||
text.append(
|
||||
"distance",
|
||||
DistanceSpan.create(
|
||||
roundValueToDistance(
|
||||
distanceMeters,
|
||||
screen.energyLevel?.distanceDisplayUnit?.value,
|
||||
carContext
|
||||
)
|
||||
),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
// power
|
||||
val power = charger.maxPower
|
||||
if (power != null) {
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append("${power.roundToInt()} kW")
|
||||
}
|
||||
|
||||
// availability
|
||||
availabilities[charger.id]?.second?.let { av ->
|
||||
val status = av.status.values.flatten()
|
||||
val available = availabilityText(status)
|
||||
val total = charger.chargepoints.sumOf { it.count }
|
||||
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append(
|
||||
"$available/$total",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
addText(text)
|
||||
setMetadata(
|
||||
Metadata.Builder()
|
||||
.setPlace(place)
|
||||
.build()
|
||||
)
|
||||
|
||||
setOnClickListener {
|
||||
screen.onChargerClick(charger)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
fun buildSingleCharger(
|
||||
charger: ChargeLocation,
|
||||
availability: ChargeLocationStatus?,
|
||||
onClick: () -> Unit
|
||||
) = Pane.Builder().apply {
|
||||
val icon = iconGen.getBitmap(
|
||||
getMarkerTint(charger),
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti(),
|
||||
fav = charger.id in favorites
|
||||
)
|
||||
|
||||
|
||||
addRow(Row.Builder().apply {
|
||||
setImage(CarIcon.Builder(IconCompat.createWithBitmap(icon)).build())
|
||||
setTitle(charger.address.toString())
|
||||
addText(generateChargepointsText(charger, availability, carContext))
|
||||
}.build())
|
||||
addAction(Action.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.show_more))
|
||||
setOnClickListener(onClick)
|
||||
}.build())
|
||||
addAction(Action.Builder().apply {
|
||||
setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_navigation
|
||||
)
|
||||
).build()
|
||||
)
|
||||
setTitle(carContext.getString(R.string.navigate))
|
||||
setBackgroundColor(CarColor.PRIMARY)
|
||||
setOnClickListener {
|
||||
navigateToCharger(carContext, charger)
|
||||
}
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
533
app/src/main/java/net/vonforst/evmap/auto/LegacyMapScreen.kt
Normal file
533
app/src/main/java/net/vonforst/evmap/auto/LegacyMapScreen.kt
Normal file
@@ -0,0 +1,533 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.info.CarInfo
|
||||
import androidx.car.app.hardware.info.CarSensors
|
||||
import androidx.car.app.hardware.info.Compass
|
||||
import androidx.car.app.hardware.info.EnergyLevel
|
||||
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.CarLocation
|
||||
import androidx.car.app.model.OnContentRefreshListener
|
||||
import androidx.car.app.model.Place
|
||||
import androidx.car.app.model.PlaceListMapTemplate
|
||||
import androidx.car.app.model.PlaceMarker
|
||||
import androidx.car.app.model.Template
|
||||
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.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.AvailabilityRepository
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.FILTERS_FAVORITES
|
||||
import net.vonforst.evmap.model.FilterValue
|
||||
import net.vonforst.evmap.model.FilterWithValue
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.bearingBetween
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import net.vonforst.evmap.utils.headingDiff
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.await
|
||||
import net.vonforst.evmap.viewmodel.awaitFinished
|
||||
import net.vonforst.evmap.viewmodel.filtersWithValue
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Main map screen showing either nearby chargers or favorites
|
||||
*
|
||||
* Legacy implementation for Car App API level < 7
|
||||
*/
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class LegacyMapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
Screen(ctx), LocationAwareScreen, OnContentRefreshListener,
|
||||
ChargerListDelegate, DefaultLifecycleObserver {
|
||||
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private var prefs = PreferenceDataSource(ctx)
|
||||
private val repo =
|
||||
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
||||
private val availabilityRepo = AvailabilityRepository(ctx)
|
||||
|
||||
private var updateCoroutine: Job? = null
|
||||
private var availabilityUpdateCoroutine: Job? = null
|
||||
|
||||
private var visibleStart: Int? = null
|
||||
private var visibleEnd: Int? = null
|
||||
|
||||
override var location: Location? = null
|
||||
private var lastDistanceUpdateTime: Instant? = null
|
||||
private var lastChargersUpdateTime: Instant? = null
|
||||
private var chargers: List<ChargeLocation>? = null
|
||||
private val favorites = db.favoritesDao().getAllFavorites()
|
||||
override var loadingError = false
|
||||
override var locationError = false
|
||||
private val searchRadius = 5 // kilometers
|
||||
private val distanceUpdateThreshold = Duration.ofSeconds(15)
|
||||
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
|
||||
private val chargersUpdateThresholdDistance = 500 // meters
|
||||
private val chargersUpdateThresholdTime = Duration.ofSeconds(30)
|
||||
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus?>> =
|
||||
HashMap()
|
||||
override val maxRows =
|
||||
min(ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST), 25)
|
||||
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
|
||||
|
||||
override var filterStatus = prefs.filterStatus
|
||||
|
||||
private var filtersWithValue: List<FilterWithValue<FilterValue>>? = null
|
||||
|
||||
private val carInfo: CarInfo by lazy {
|
||||
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
|
||||
}
|
||||
private val carSensors: CarSensors by lazy { carContext.patchedCarSensors }
|
||||
override var energyLevel: EnergyLevel? = null
|
||||
private var heading: Compass? = null
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
||||
private var searchLocation: LatLng? = null
|
||||
|
||||
private val formatter = ChargerListFormatter(ctx, this)
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
marker = MapScreen.MARKER
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
session.mapScreen = this
|
||||
return PlaceListMapTemplate.Builder().apply {
|
||||
setTitle(
|
||||
prefs.placeSearchResultAndroidAutoName?.let {
|
||||
carContext.getString(R.string.auto_chargers_near_location, it)
|
||||
} ?: carContext.getString(
|
||||
if (filterStatus == FILTERS_FAVORITES) {
|
||||
R.string.auto_favorites
|
||||
} else {
|
||||
R.string.auto_chargers_closeby
|
||||
}
|
||||
)
|
||||
)
|
||||
if (prefs.placeSearchResultAndroidAutoName != null) {
|
||||
searchLocation?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
|
||||
if (prefs.placeSearchResultAndroidAutoName != null) {
|
||||
setMarker(
|
||||
PlaceMarker.Builder()
|
||||
.setColor(CarColor.PRIMARY)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
} else {
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).build())
|
||||
}
|
||||
}
|
||||
formatter.buildChargerList(chargers, availabilities)?.let {
|
||||
setItemList(it)
|
||||
} ?: setLoading(true)
|
||||
setCurrentLocationEnabled(true)
|
||||
setHeaderAction(Action.APP_ICON)
|
||||
val filtersCount = if (filterStatus == FILTERS_FAVORITES) 1 else {
|
||||
filtersWithValue?.count {
|
||||
!it.value.hasSameValueAs(it.filter.defaultValue())
|
||||
}
|
||||
}
|
||||
|
||||
setActionStrip(
|
||||
ActionStrip.Builder()
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_settings
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
screenManager.push(SettingsScreen(carContext, session))
|
||||
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 {
|
||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
||||
prefs.placeSearchResultAndroidAutoName = null
|
||||
prefs.placeSearchResultAndroidAuto = null
|
||||
if (!supportsRefresh) {
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
} else {
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
} else {
|
||||
screenManager.pushForResult(
|
||||
PlaceSearchScreen(
|
||||
carContext,
|
||||
session
|
||||
)
|
||||
) {
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
session.mapScreen = null
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_filter
|
||||
)
|
||||
)
|
||||
.setTint(if (filtersCount != null && filtersCount > 0) CarColor.SECONDARY else CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
screenManager.push(FilterScreen(carContext, session))
|
||||
session.mapScreen = null
|
||||
}
|
||||
.build())
|
||||
.build())
|
||||
if (carContext.carAppApiLevel >= 5 ||
|
||||
(BuildConfig.FLAVOR_automotive == "automotive" && carContext.carAppApiLevel >= 4)
|
||||
) {
|
||||
setOnContentRefreshListener(this@LegacyMapScreen)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun onChargerClick(charger: ChargeLocation) {
|
||||
screenManager.push(ChargerDetailScreen(carContext, charger))
|
||||
session.mapScreen = null
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
if (location.latitude == this.location?.latitude
|
||||
&& location.longitude == this.location?.longitude
|
||||
) {
|
||||
return
|
||||
}
|
||||
val previousLocation = this.location
|
||||
this.location = location
|
||||
if (previousLocation == null) {
|
||||
loadChargers()
|
||||
return
|
||||
}
|
||||
|
||||
val now = Instant.now()
|
||||
if (lastDistanceUpdateTime == null ||
|
||||
Duration.between(lastDistanceUpdateTime, now) > distanceUpdateThreshold
|
||||
) {
|
||||
lastDistanceUpdateTime = now
|
||||
// update displayed distances
|
||||
invalidate()
|
||||
}
|
||||
|
||||
// if chargers are searched around current location, consider app-driven refresh
|
||||
val searchLocation =
|
||||
if (prefs.placeSearchResultAndroidAuto == null) searchLocation else null
|
||||
val distance = searchLocation?.let {
|
||||
distanceBetween(
|
||||
it.latitude, it.longitude, location.latitude, location.longitude
|
||||
)
|
||||
} ?: 0.0
|
||||
if (supportsRefresh && (lastChargersUpdateTime == null ||
|
||||
Duration.between(
|
||||
lastChargersUpdateTime,
|
||||
now
|
||||
) > chargersUpdateThresholdTime) && (distance > chargersUpdateThresholdDistance)
|
||||
) {
|
||||
onContentRefreshRequested()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadChargers() {
|
||||
val location = location ?: return
|
||||
|
||||
val searchLocation =
|
||||
prefs.placeSearchResultAndroidAuto?.latLng ?: LatLng.fromLocation(location)
|
||||
this.searchLocation = searchLocation
|
||||
|
||||
updateCoroutine = lifecycleScope.launch {
|
||||
loadingError = false
|
||||
try {
|
||||
filterStatus = prefs.filterStatus
|
||||
val filterValues =
|
||||
db.filterValueDao().getFilterValuesAsync(filterStatus, prefs.dataSource)
|
||||
val filters = repo.getFiltersAsync(carContext.stringProvider())
|
||||
filtersWithValue = filtersWithValue(filters, filterValues)
|
||||
|
||||
val apiId = repo.api.value!!.id
|
||||
|
||||
// load chargers
|
||||
if (filterStatus == FILTERS_FAVORITES) {
|
||||
val chargers = favorites.await().map { it.charger }.sortedBy {
|
||||
distanceBetween(
|
||||
location.latitude, location.longitude,
|
||||
it.coordinates.lat, it.coordinates.lng
|
||||
)
|
||||
}
|
||||
this@LegacyMapScreen.chargers = chargers
|
||||
} else {
|
||||
// try multiple search radii until we have enough chargers
|
||||
var chargers: List<ChargeLocation>? = null
|
||||
val radiusValues = listOf(searchRadius, searchRadius * 10, searchRadius * 50)
|
||||
for (radius in radiusValues) {
|
||||
val response = repo.getChargepointsRadius(
|
||||
searchLocation,
|
||||
radius,
|
||||
zoom = 16f,
|
||||
filtersWithValue
|
||||
).awaitFinished()
|
||||
if (response.status == Status.ERROR && if (radius == radiusValues.last()) response.data.isNullOrEmpty() else response.data == null) {
|
||||
loadingError = true
|
||||
this@LegacyMapScreen.chargers = null
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
if (prefs.placeSearchResultAndroidAutoName == null) {
|
||||
chargers = headingFilter(
|
||||
chargers,
|
||||
searchLocation
|
||||
)
|
||||
}
|
||||
if (chargers == null || chargers.size >= maxRows) {
|
||||
break
|
||||
}
|
||||
}
|
||||
this@LegacyMapScreen.chargers = chargers
|
||||
}
|
||||
|
||||
updateCoroutine = null
|
||||
lastChargersUpdateTime = Instant.now()
|
||||
lastDistanceUpdateTime = Instant.now()
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
loadingError = true
|
||||
invalidate()
|
||||
} catch (e: HttpException) {
|
||||
loadingError = true
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters by heading if heading available and enabled
|
||||
*/
|
||||
private fun headingFilter(
|
||||
chargers: List<ChargeLocation>?,
|
||||
searchLocation: LatLng
|
||||
): List<ChargeLocation>? {
|
||||
// use compass heading if available, otherwise fall back to GPS
|
||||
val location = location
|
||||
val heading = heading?.orientations?.value?.get(0)
|
||||
?: if (location?.hasBearing() == true) location.bearing else null
|
||||
return heading?.let {
|
||||
if (!prefs.showChargersAheadAndroidAuto) return@let chargers
|
||||
|
||||
chargers?.filter {
|
||||
val bearing = bearingBetween(
|
||||
searchLocation.latitude,
|
||||
searchLocation.longitude,
|
||||
it.coordinates.lat,
|
||||
it.coordinates.lng
|
||||
)
|
||||
val diff = headingDiff(bearing, heading.toDouble())
|
||||
abs(diff) < 30
|
||||
}
|
||||
} ?: chargers
|
||||
}
|
||||
|
||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
||||
val isUpdate = this.energyLevel == null
|
||||
this.energyLevel = energyLevel
|
||||
if (isUpdate) invalidate()
|
||||
}
|
||||
|
||||
private fun onCompassUpdated(compass: Compass) {
|
||||
this.heading = compass
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
setupListeners()
|
||||
session.requestLocationUpdates()
|
||||
locationError = false
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
if (location == null) {
|
||||
locationError = true
|
||||
invalidate()
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
// Reloading chargers in onStart does not seem to count towards content limit.
|
||||
// So let's do this so the user gets fresh chargers when re-entering the app.
|
||||
if (prefs.dataSource != repo.api.value?.id) {
|
||||
repo.api.value = createApi(prefs.dataSource, carContext)
|
||||
}
|
||||
invalidate()
|
||||
loadChargers()
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
carSensors.addCompassListener(
|
||||
CarSensors.UPDATE_RATE_NORMAL,
|
||||
exec,
|
||||
::onCompassUpdated
|
||||
)
|
||||
}
|
||||
if (!permissions.all {
|
||||
ContextCompat.checkSelfPermission(
|
||||
carContext,
|
||||
it
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
})
|
||||
return
|
||||
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
println("Setting up energy level listener")
|
||||
carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
// Reloading chargers in onStart does not seem to count towards content limit.
|
||||
// So let's do this so the user gets fresh chargers when re-entering the app.
|
||||
// Deleting the data already in onStop makes sure that we show a loading screen directly
|
||||
// (i.e. onGetTemplate is not called while the old data is still there)
|
||||
chargers = null
|
||||
availabilities.clear()
|
||||
location = null
|
||||
removeListeners()
|
||||
}
|
||||
|
||||
private fun removeListeners() {
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
println("Removing energy level listener")
|
||||
carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
carSensors.removeCompassListener(::onCompassUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
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 && availabilities.isNotEmpty()) return
|
||||
if (startIndex == -1 || endIndex == -1) return
|
||||
if (availabilityUpdateCoroutine != null) return
|
||||
|
||||
visibleEnd = endIndex
|
||||
visibleStart = startIndex
|
||||
|
||||
// remove outdated availabilities
|
||||
availabilities = availabilities.filter {
|
||||
Duration.between(
|
||||
it.value.first,
|
||||
ZonedDateTime.now()
|
||||
) <= availabilityUpdateThreshold
|
||||
}.toMutableMap()
|
||||
|
||||
// update availabilities
|
||||
availabilityUpdateCoroutine = lifecycleScope.launch {
|
||||
delay(300L)
|
||||
|
||||
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 {
|
||||
val availability = availabilityRepo.getAvailability(it).data
|
||||
val date = ZonedDateTime.now()
|
||||
availabilities[it.id] = date to availability
|
||||
}
|
||||
} else null
|
||||
}
|
||||
if (tasks.isNotEmpty()) {
|
||||
tasks.awaitAll()
|
||||
invalidate()
|
||||
}
|
||||
availabilityUpdateCoroutine = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.Header
|
||||
import androidx.car.app.model.ItemList
|
||||
import androidx.car.app.model.ListTemplate
|
||||
import androidx.car.app.model.ParkedOnlyOnClickListener
|
||||
import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.Template
|
||||
import com.car2go.maps.AttributionClickListener
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
class MapAttributionScreen(
|
||||
ctx: CarContext,
|
||||
val attributions: List<AttributionClickListener.Attribution>
|
||||
) : Screen(ctx) {
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder()
|
||||
.setHeader(
|
||||
Header.Builder()
|
||||
.setStartHeaderAction(Action.BACK)
|
||||
.setTitle(carContext.getString(R.string.maplibre_attributionsDialogTitle))
|
||||
.build()
|
||||
)
|
||||
.setSingleList(ItemList.Builder().apply {
|
||||
attributions.forEach { attr ->
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(attr.title)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener(
|
||||
ParkedOnlyOnClickListener.create {
|
||||
openUrl(carContext, attr.url)
|
||||
}).build()
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +1,44 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.location.Location
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.car.app.AppManager
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.annotations.ExperimentalCarApi
|
||||
import androidx.car.app.annotations.RequiresCarApi
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.info.CarInfo
|
||||
import androidx.car.app.hardware.info.CarSensors
|
||||
import androidx.car.app.hardware.info.Compass
|
||||
import androidx.car.app.hardware.info.EnergyLevel
|
||||
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.Header
|
||||
import androidx.car.app.model.ListTemplate
|
||||
import androidx.car.app.model.MessageTemplate
|
||||
import androidx.car.app.model.PaneTemplate
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.navigation.model.MapController
|
||||
import androidx.car.app.navigation.model.MapWithContentTemplate
|
||||
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.AnyMap
|
||||
import com.car2go.maps.OnMapReadyCallback
|
||||
import com.car2go.maps.model.LatLng
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.AvailabilityRepository
|
||||
@@ -30,19 +46,18 @@ import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.ChargeLocationCluster
|
||||
import net.vonforst.evmap.model.ChargepointListItem
|
||||
import net.vonforst.evmap.model.FILTERS_FAVORITES
|
||||
import net.vonforst.evmap.model.FilterValue
|
||||
import net.vonforst.evmap.model.FilterWithValue
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.availabilityText
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.utils.bearingBetween
|
||||
import net.vonforst.evmap.ui.MarkerManager
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import net.vonforst.evmap.utils.headingDiff
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.await
|
||||
import net.vonforst.evmap.viewmodel.awaitFinished
|
||||
import net.vonforst.evmap.viewmodel.filtersWithValue
|
||||
import retrofit2.HttpException
|
||||
@@ -51,58 +66,62 @@ import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Main map screen showing either nearby chargers or favorites
|
||||
* Main map screen showing either nearby chargers or favorites.
|
||||
*
|
||||
* New implementation for Car App API Level >= 7 with interactive map using MapSurfaceCallback
|
||||
*/
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
@RequiresCarApi(7)
|
||||
@ExperimentalCarApi
|
||||
class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
Screen(ctx), LocationAwareScreen, OnContentRefreshListener,
|
||||
ItemList.OnItemVisibilityChangedListener, DefaultLifecycleObserver {
|
||||
Screen(ctx), LocationAwareScreen, ChargerListDelegate,
|
||||
DefaultLifecycleObserver, OnMapReadyCallback {
|
||||
companion object {
|
||||
val MARKER = "map"
|
||||
}
|
||||
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private var prefs = PreferenceDataSource(ctx)
|
||||
private val repo =
|
||||
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
||||
private val availabilityRepo = AvailabilityRepository(ctx)
|
||||
|
||||
private var updateCoroutine: Job? = null
|
||||
private var availabilityUpdateCoroutine: Job? = null
|
||||
|
||||
private var visibleStart: Int? = null
|
||||
private var visibleEnd: Int? = null
|
||||
|
||||
private var location: Location? = null
|
||||
override var location: Location? = null
|
||||
private var lastDistanceUpdateTime: Instant? = null
|
||||
private var lastChargersUpdateTime: Instant? = null
|
||||
private var chargers: List<ChargeLocation>? = null
|
||||
private var isFavorite: List<Boolean>? = null
|
||||
private var loadingError = false
|
||||
private var locationError = false
|
||||
private var prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val repo =
|
||||
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
||||
private val availabilityRepo = AvailabilityRepository(ctx)
|
||||
private val searchRadius = 5 // kilometers
|
||||
private var chargers: List<ChargepointListItem>? = null
|
||||
private var selectedCharger: ChargeLocation? = null
|
||||
private val favorites = db.favoritesDao().getAllFavorites()
|
||||
|
||||
override var loadingError = false
|
||||
override val locationError = false
|
||||
|
||||
private val mapSurfaceCallback = MapSurfaceCallback(carContext, lifecycleScope)
|
||||
|
||||
private val distanceUpdateThreshold = Duration.ofSeconds(15)
|
||||
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
|
||||
private val chargersUpdateThresholdDistance = 500 // meters
|
||||
private val chargersUpdateThresholdTime = Duration.ofSeconds(30)
|
||||
|
||||
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus?>> =
|
||||
HashMap()
|
||||
private val maxRows =
|
||||
override val maxRows =
|
||||
min(ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST), 25)
|
||||
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
|
||||
|
||||
private var filterStatus = prefs.filterStatus
|
||||
override var filterStatus = prefs.filterStatus
|
||||
private var filtersWithValue: List<FilterWithValue<FilterValue>>? = null
|
||||
|
||||
private val carInfo: CarInfo by lazy {
|
||||
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
|
||||
}
|
||||
private val carSensors: CarSensors by lazy { carContext.patchedCarSensors }
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
override var energyLevel: EnergyLevel? = null
|
||||
|
||||
private var heading: Compass? = null
|
||||
private val permissions = if (BuildConfig.FLAVOR_automotive == "automotive") {
|
||||
listOf(
|
||||
@@ -116,280 +135,234 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
)
|
||||
}
|
||||
|
||||
private var searchLocation: LatLng? = null
|
||||
private var map: AnyMap? = null
|
||||
private var markerManager: MarkerManager? = null
|
||||
private var myLocationEnabled = false
|
||||
private var myLocationNeedsUpdate = false
|
||||
|
||||
private val iconGen =
|
||||
ChargerIconGenerator(carContext, null, height = 96)
|
||||
private val formatter = ChargerListFormatter(ctx, this)
|
||||
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
clearSelectedCharger()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
marker = MARKER
|
||||
|
||||
favorites.observe(this) {
|
||||
val favoriteIds = it.map { it.favorite.chargerId }.toSet()
|
||||
markerManager?.favorites = favoriteIds
|
||||
formatter.favorites = favoriteIds
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
carContext.getCarService(AppManager::class.java)
|
||||
.setSurfaceCallback(mapSurfaceCallback)
|
||||
|
||||
carContext.onBackPressedDispatcher.addCallback(this, backPressedCallback)
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
session.mapScreen = this
|
||||
return PlaceListMapTemplate.Builder().apply {
|
||||
setTitle(
|
||||
prefs.placeSearchResultAndroidAutoName?.let {
|
||||
carContext.getString(R.string.auto_chargers_near_location, it)
|
||||
} ?: carContext.getString(
|
||||
if (filterStatus == FILTERS_FAVORITES) {
|
||||
R.string.auto_favorites
|
||||
} else {
|
||||
R.string.auto_chargers_closeby
|
||||
}
|
||||
)
|
||||
)
|
||||
if (prefs.placeSearchResultAndroidAutoName != null) {
|
||||
searchLocation?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
|
||||
if (prefs.placeSearchResultAndroidAutoName != null) {
|
||||
setMarker(
|
||||
PlaceMarker.Builder()
|
||||
.setColor(CarColor.PRIMARY)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
val map = map
|
||||
|
||||
val title = prefs.placeSearchResultAndroidAutoName ?: carContext.getString(
|
||||
if (filterStatus == FILTERS_FAVORITES) {
|
||||
R.string.auto_favorites
|
||||
} else if (myLocationEnabled) {
|
||||
R.string.auto_chargers_closeby
|
||||
} else {
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).build())
|
||||
}
|
||||
}
|
||||
chargers?.take(maxRows)?.let { chargerList ->
|
||||
val builder = ItemList.Builder()
|
||||
// only show the city if not all chargers are in the same city
|
||||
val showCity = chargerList.map { it.address?.city }.distinct().size > 1
|
||||
chargerList.forEachIndexed { i, charger ->
|
||||
builder.addItem(formatCharger(charger, showCity, isFavorite?.get(i) ?: false))
|
||||
}
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(
|
||||
if (filterStatus == FILTERS_FAVORITES) {
|
||||
R.string.auto_no_favorites_found
|
||||
} else {
|
||||
R.string.auto_no_chargers_found
|
||||
}
|
||||
)
|
||||
)
|
||||
builder.setOnItemsVisibilityChangedListener(this@MapScreen)
|
||||
setItemList(builder.build())
|
||||
} ?: run {
|
||||
if (loadingError) {
|
||||
val builder = ItemList.Builder()
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(R.string.connection_error)
|
||||
)
|
||||
setItemList(builder.build())
|
||||
} else if (locationError) {
|
||||
val builder = ItemList.Builder()
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(R.string.location_error)
|
||||
)
|
||||
setItemList(builder.build())
|
||||
} else {
|
||||
setLoading(true)
|
||||
}
|
||||
}
|
||||
setCurrentLocationEnabled(true)
|
||||
setHeaderAction(Action.APP_ICON)
|
||||
val filtersCount = if (filterStatus == FILTERS_FAVORITES) 1 else {
|
||||
filtersWithValue?.count {
|
||||
!it.value.hasSameValueAs(it.filter.defaultValue())
|
||||
}
|
||||
R.string.app_name
|
||||
}
|
||||
)
|
||||
|
||||
setActionStrip(
|
||||
ActionStrip.Builder()
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_settings
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
screenManager.push(SettingsScreen(carContext, session))
|
||||
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()
|
||||
val actionStrip = buildActionStrip()
|
||||
val selectedCharger = selectedCharger
|
||||
|
||||
)
|
||||
setOnClickListener {
|
||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
||||
prefs.placeSearchResultAndroidAutoName = null
|
||||
prefs.placeSearchResultAndroidAuto = null
|
||||
if (!supportsRefresh) {
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
chargers = null
|
||||
isFavorite = null
|
||||
loadChargers()
|
||||
}
|
||||
} else {
|
||||
chargers = null
|
||||
isFavorite = null
|
||||
loadChargers()
|
||||
}
|
||||
} else {
|
||||
screenManager.pushForResult(
|
||||
PlaceSearchScreen(
|
||||
carContext,
|
||||
session
|
||||
)
|
||||
) {
|
||||
chargers = null
|
||||
isFavorite = null
|
||||
loadChargers()
|
||||
}
|
||||
session.mapScreen = null
|
||||
}
|
||||
}
|
||||
val contentTemplate = if (selectedCharger != null) {
|
||||
PaneTemplate.Builder(
|
||||
formatter.buildSingleCharger(
|
||||
selectedCharger,
|
||||
availabilities.get(selectedCharger.id)?.second
|
||||
) {
|
||||
screenManager.push(ChargerDetailScreen(carContext, selectedCharger))
|
||||
session.mapScreen = null
|
||||
}).apply {
|
||||
setHeader(Header.Builder().apply {
|
||||
setTitle(selectedCharger.name)
|
||||
setStartHeaderAction(Action.BACK)
|
||||
}.build())
|
||||
}.build()
|
||||
} else if (chargers?.filterIsInstance<ChargeLocationCluster>()?.isNotEmpty() == true) {
|
||||
MessageTemplate.Builder(carContext.getString(R.string.auto_zoom_for_details))
|
||||
.apply {
|
||||
setHeader(Header.Builder().apply {
|
||||
setTitle(title)
|
||||
setStartHeaderAction(Action.APP_ICON)
|
||||
}.build())
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_filter
|
||||
)
|
||||
)
|
||||
.setTint(if (filtersCount != null && filtersCount > 0) CarColor.SECONDARY else CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
screenManager.push(FilterScreen(carContext, session))
|
||||
session.mapScreen = null
|
||||
}
|
||||
.build())
|
||||
.build())
|
||||
if (carContext.carAppApiLevel >= 5 ||
|
||||
(BuildConfig.FLAVOR_automotive == "automotive" && carContext.carAppApiLevel >= 4)
|
||||
) {
|
||||
setOnContentRefreshListener(this@MapScreen)
|
||||
}
|
||||
}.build()
|
||||
} else {
|
||||
ListTemplate.Builder().apply {
|
||||
setHeader(Header.Builder().apply {
|
||||
setTitle(title)
|
||||
setStartHeaderAction(Action.APP_ICON)
|
||||
}.build())
|
||||
|
||||
formatter.buildChargerList(
|
||||
chargers?.filterIsInstance<ChargeLocation>(),
|
||||
availabilities
|
||||
)?.let {
|
||||
setSingleList(it)
|
||||
} ?: setLoading(true)
|
||||
}.build()
|
||||
}
|
||||
return MapWithContentTemplate.Builder().apply {
|
||||
setContentTemplate(contentTemplate)
|
||||
setActionStrip(actionStrip)
|
||||
setMapController(MapController.Builder().apply {
|
||||
setMapActionStrip(buildMapActionStrip())
|
||||
setPanModeListener { }
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun formatCharger(
|
||||
charger: ChargeLocation,
|
||||
showCity: Boolean,
|
||||
isFavorite: Boolean
|
||||
): Row {
|
||||
val markerTint = getMarkerTint(charger)
|
||||
val backgroundTint = if ((charger.maxPower ?: 0.0) > 100) {
|
||||
R.color.charger_100kw_dark // slightly darker color for better contrast
|
||||
} else {
|
||||
markerTint
|
||||
}
|
||||
val color = ContextCompat.getColor(carContext, backgroundTint)
|
||||
val place =
|
||||
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
|
||||
.setMarker(
|
||||
PlaceMarker.Builder()
|
||||
.setColor(CarColor.createCustom(color, color))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
val icon = iconGen.getBitmap(
|
||||
markerTint,
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti(),
|
||||
fav = isFavorite
|
||||
)
|
||||
val iconSpan =
|
||||
CarIconSpan.create(CarIcon.Builder(IconCompat.createWithBitmap(icon)).build())
|
||||
|
||||
return Row.Builder().apply {
|
||||
// only show the city if not all chargers are in the same city (-> showCity == true)
|
||||
// and the city is not already contained in the charger name
|
||||
val title = SpannableStringBuilder().apply {
|
||||
append(" ", iconSpan, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
append(" ")
|
||||
append(charger.name)
|
||||
}
|
||||
if (showCity && charger.address?.city != null && charger.address.city !in charger.name) {
|
||||
val titleWithCity = SpannableStringBuilder().apply {
|
||||
append("", iconSpan, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
append(" ")
|
||||
append("${charger.name} · ${charger.address.city}")
|
||||
}
|
||||
setTitle(CarText.Builder(titleWithCity).addVariant(title).build())
|
||||
} else {
|
||||
setTitle(title)
|
||||
}
|
||||
|
||||
val text = SpannableStringBuilder()
|
||||
|
||||
// distance
|
||||
location?.let {
|
||||
val distanceMeters = distanceBetween(
|
||||
it.latitude, it.longitude,
|
||||
charger.coordinates.lat, charger.coordinates.lng
|
||||
)
|
||||
text.append(
|
||||
"distance",
|
||||
DistanceSpan.create(
|
||||
roundValueToDistance(
|
||||
distanceMeters,
|
||||
energyLevel?.distanceDisplayUnit?.value,
|
||||
carContext
|
||||
)
|
||||
),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
// power
|
||||
val power = charger.maxPower
|
||||
if (power != null) {
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append("${power.roundToInt()} kW")
|
||||
}
|
||||
|
||||
// availability
|
||||
availabilities[charger.id]?.second?.let { av ->
|
||||
val status = av.status.values.flatten()
|
||||
val available = availabilityText(status)
|
||||
val total = charger.chargepoints.sumOf { it.count }
|
||||
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append(
|
||||
"$available/$total",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
addText(text)
|
||||
setMetadata(
|
||||
Metadata.Builder()
|
||||
.setPlace(place)
|
||||
private fun buildMapActionStrip() = ActionStrip.Builder()
|
||||
.addAction(Action.PAN)
|
||||
.addAction(
|
||||
Action.Builder().setIcon(
|
||||
CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_location))
|
||||
.setTint(if (myLocationEnabled) CarColor.SECONDARY else CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
).setOnClickListener {
|
||||
enableLocation(true)
|
||||
}.build()
|
||||
)
|
||||
.addAction(
|
||||
Action.Builder().setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_add
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
).setOnClickListener {
|
||||
val map = map ?: return@setOnClickListener
|
||||
mapSurfaceCallback.animateCamera(map.cameraUpdateFactory.zoomBy(0.5f))
|
||||
}.build()
|
||||
)
|
||||
.addAction(
|
||||
Action.Builder().setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_remove
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
).setOnClickListener {
|
||||
val map = map ?: return@setOnClickListener
|
||||
mapSurfaceCallback.animateCamera(map.cameraUpdateFactory.zoomBy(-0.5f))
|
||||
}.build()
|
||||
).build()
|
||||
|
||||
setOnClickListener {
|
||||
screenManager.push(ChargerDetailScreen(carContext, charger))
|
||||
session.mapScreen = null
|
||||
private fun buildActionStrip(): ActionStrip {
|
||||
val filtersCount = if (filterStatus == FILTERS_FAVORITES) 1 else {
|
||||
filtersWithValue?.count {
|
||||
!it.value.hasSameValueAs(it.filter.defaultValue())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
return ActionStrip.Builder()
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_settings
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
screenManager.push(SettingsScreen(carContext, session))
|
||||
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 {
|
||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
||||
prefs.placeSearchResultAndroidAutoName = null
|
||||
prefs.placeSearchResultAndroidAuto = null
|
||||
markerManager?.searchResult = null
|
||||
invalidate()
|
||||
} else {
|
||||
screenManager.pushForResult(
|
||||
PlaceSearchScreen(
|
||||
carContext,
|
||||
session
|
||||
)
|
||||
) {
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
session.mapScreen = null
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_filter
|
||||
)
|
||||
)
|
||||
.setTint(if (filtersCount != null && filtersCount > 0) CarColor.SECONDARY else CarColor.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
screenManager.push(FilterScreen(carContext, session))
|
||||
session.mapScreen = null
|
||||
}
|
||||
.build())
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onChargerClick(charger: ChargeLocation) {
|
||||
selectedCharger = charger
|
||||
markerManager?.highlighedCharger = charger
|
||||
markerManager?.animateBounce(charger)
|
||||
backPressedCallback.isEnabled = true
|
||||
invalidate()
|
||||
// load availability
|
||||
lifecycleScope.launch {
|
||||
val availability = availabilityRepo.getAvailability(charger).data
|
||||
val date = ZonedDateTime.now()
|
||||
availabilities[charger.id] = date to availability
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSelectedCharger() {
|
||||
selectedCharger = null
|
||||
markerManager?.highlighedCharger = null
|
||||
backPressedCallback.isEnabled = false
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
@@ -398,11 +371,25 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
) {
|
||||
return
|
||||
}
|
||||
val previousLocation = this.location
|
||||
val oldLoc = this.location?.let { LatLng.fromLocation(it) }
|
||||
val latLng = LatLng.fromLocation(location)
|
||||
this.location = location
|
||||
if (previousLocation == null) {
|
||||
loadChargers()
|
||||
return
|
||||
|
||||
val map = map ?: return
|
||||
if (myLocationEnabled) {
|
||||
if (oldLoc == null) {
|
||||
mapSurfaceCallback.animateCamera(map.cameraUpdateFactory.newLatLngZoom(latLng, 13f))
|
||||
} else if (latLng != oldLoc && distanceBetween(
|
||||
latLng.latitude,
|
||||
latLng.longitude,
|
||||
oldLoc.latitude,
|
||||
oldLoc.longitude
|
||||
) > 1
|
||||
) {
|
||||
// only update map if location changed by more than 1 meter
|
||||
val camUpdate = map.cameraUpdateFactory.newLatLng(latLng)
|
||||
mapSurfaceCallback.animateCamera(camUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
val now = Instant.now()
|
||||
@@ -413,31 +400,11 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
// update displayed distances
|
||||
invalidate()
|
||||
}
|
||||
|
||||
// if chargers are searched around current location, consider app-driven refresh
|
||||
val searchLocation =
|
||||
if (prefs.placeSearchResultAndroidAuto == null) searchLocation else null
|
||||
val distance = searchLocation?.let {
|
||||
distanceBetween(
|
||||
it.latitude, it.longitude, location.latitude, location.longitude
|
||||
)
|
||||
} ?: 0.0
|
||||
if (supportsRefresh && (lastChargersUpdateTime == null ||
|
||||
Duration.between(
|
||||
lastChargersUpdateTime,
|
||||
now
|
||||
) > chargersUpdateThresholdTime) && (distance > chargersUpdateThresholdDistance)
|
||||
) {
|
||||
onContentRefreshRequested()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadChargers() {
|
||||
val location = location ?: return
|
||||
|
||||
val searchLocation =
|
||||
prefs.placeSearchResultAndroidAuto ?: LatLng.fromLocation(location)
|
||||
this.searchLocation = searchLocation
|
||||
val map = map ?: return
|
||||
|
||||
updateCoroutine = lifecycleScope.launch {
|
||||
loadingError = false
|
||||
@@ -448,56 +415,33 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
val filters = repo.getFiltersAsync(carContext.stringProvider())
|
||||
filtersWithValue = filtersWithValue(filters, filterValues)
|
||||
|
||||
val apiId = repo.api.value!!.id
|
||||
|
||||
// load chargers
|
||||
if (filterStatus == FILTERS_FAVORITES) {
|
||||
val chargers =
|
||||
db.favoritesDao().getAllFavoritesAsync().map { it.charger }.sortedBy {
|
||||
distanceBetween(
|
||||
location.latitude, location.longitude,
|
||||
it.coordinates.lat, it.coordinates.lng
|
||||
)
|
||||
}
|
||||
val chargers = favorites.await().map { it.charger }.sortedBy {
|
||||
distanceBetween(
|
||||
location.latitude, location.longitude,
|
||||
it.coordinates.lat, it.coordinates.lng
|
||||
)
|
||||
}
|
||||
this@MapScreen.chargers = chargers
|
||||
isFavorite = List(chargers.size) { true }
|
||||
} else {
|
||||
// try multiple search radii until we have enough chargers
|
||||
var chargers: List<ChargeLocation>? = null
|
||||
val radiusValues = listOf(searchRadius, searchRadius * 10, searchRadius * 50)
|
||||
for (radius in radiusValues) {
|
||||
val response = repo.getChargepointsRadius(
|
||||
searchLocation,
|
||||
radius,
|
||||
zoom = 16f,
|
||||
filtersWithValue
|
||||
).awaitFinished()
|
||||
if (response.status == Status.ERROR && if (radius == radiusValues.last()) response.data.isNullOrEmpty() else response.data == null) {
|
||||
loadingError = true
|
||||
this@MapScreen.chargers = null
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
if (prefs.placeSearchResultAndroidAutoName == null) {
|
||||
chargers = headingFilter(
|
||||
chargers,
|
||||
searchLocation
|
||||
)
|
||||
}
|
||||
if (chargers == null || chargers.size >= maxRows) {
|
||||
break
|
||||
}
|
||||
val response = repo.getChargepoints(
|
||||
map.projection.visibleRegion.latLngBounds,
|
||||
map.cameraPosition.zoom,
|
||||
filtersWithValue,
|
||||
false
|
||||
).awaitFinished()
|
||||
if (response.status == Status.ERROR || response.data == null) {
|
||||
loadingError = true
|
||||
this@MapScreen.chargers = null
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
val isFavorite = chargers?.map {
|
||||
db.favoritesDao().findFavorite(it.id, apiId) != null
|
||||
}
|
||||
this@MapScreen.chargers = chargers
|
||||
this@MapScreen.isFavorite = isFavorite
|
||||
this@MapScreen.chargers = response.data
|
||||
markerManager?.chargepoints = response.data
|
||||
}
|
||||
|
||||
updateCoroutine = null
|
||||
lastChargersUpdateTime = Instant.now()
|
||||
lastDistanceUpdateTime = Instant.now()
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
@@ -510,33 +454,6 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters by heading if heading available and enabled
|
||||
*/
|
||||
private fun headingFilter(
|
||||
chargers: List<ChargeLocation>?,
|
||||
searchLocation: LatLng
|
||||
): List<ChargeLocation>? {
|
||||
// use compass heading if available, otherwise fall back to GPS
|
||||
val location = location
|
||||
val heading = heading?.orientations?.value?.get(0)
|
||||
?: if (location?.hasBearing() == true) location.bearing else null
|
||||
return heading?.let {
|
||||
if (!prefs.showChargersAheadAndroidAuto) return@let chargers
|
||||
|
||||
chargers?.filter {
|
||||
val bearing = bearingBetween(
|
||||
searchLocation.latitude,
|
||||
searchLocation.longitude,
|
||||
it.coordinates.lat,
|
||||
it.coordinates.lng
|
||||
)
|
||||
val diff = headingDiff(bearing, heading.toDouble())
|
||||
abs(diff) < 30
|
||||
}
|
||||
} ?: chargers
|
||||
}
|
||||
|
||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
||||
val isUpdate = this.energyLevel == null
|
||||
this.energyLevel = energyLevel
|
||||
@@ -548,15 +465,9 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
mapSurfaceCallback.getMapAsync(this)
|
||||
setupListeners()
|
||||
session.requestLocationUpdates()
|
||||
locationError = false
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
if (location == null) {
|
||||
locationError = true
|
||||
invalidate()
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
// Reloading chargers in onStart does not seem to count towards content limit.
|
||||
// So let's do this so the user gets fresh chargers when re-entering the app.
|
||||
@@ -564,7 +475,6 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
repo.api.value = createApi(prefs.dataSource, carContext)
|
||||
}
|
||||
invalidate()
|
||||
loadChargers()
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
@@ -598,9 +508,20 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
chargers = null
|
||||
availabilities.clear()
|
||||
location = null
|
||||
myLocationEnabled = false
|
||||
removeListeners()
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
super.onPause(owner)
|
||||
|
||||
map?.let {
|
||||
prefs.currentMapLocation = it.cameraPosition.target
|
||||
prefs.currentMapZoom = it.cameraPosition.zoom
|
||||
}
|
||||
prefs.currentMapMyLocationEnabled = myLocationEnabled
|
||||
}
|
||||
|
||||
private fun removeListeners() {
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
println("Removing energy level listener")
|
||||
@@ -609,17 +530,6 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
}
|
||||
|
||||
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 && availabilities.isNotEmpty()) return
|
||||
@@ -641,7 +551,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
availabilityUpdateCoroutine = lifecycleScope.launch {
|
||||
delay(300L)
|
||||
|
||||
val chargers = chargers ?: return@launch
|
||||
val chargers = chargers?.filterIsInstance(ChargeLocation::class.java) ?: return@launch
|
||||
if (chargers.isEmpty()) return@launch
|
||||
|
||||
val tasks = chargers.subList(
|
||||
@@ -664,4 +574,97 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
availabilityUpdateCoroutine = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMapReady(map: AnyMap) {
|
||||
this.map = map
|
||||
this.markerManager =
|
||||
MarkerManager(
|
||||
mapSurfaceCallback.presentation.context,
|
||||
map,
|
||||
this,
|
||||
markerHeight = if (BuildConfig.FLAVOR_automotive == "automotive") 36 else 64
|
||||
).apply {
|
||||
this@MapScreen.chargers?.let { chargepoints = it }
|
||||
onChargerClick = this@MapScreen::onChargerClick
|
||||
onClusterClick = {
|
||||
val newZoom = map.cameraPosition.zoom + 2
|
||||
mapSurfaceCallback.animateCamera(
|
||||
map.cameraUpdateFactory.newLatLngZoom(
|
||||
LatLng(it.coordinates.lat, it.coordinates.lng),
|
||||
newZoom
|
||||
)
|
||||
)
|
||||
}
|
||||
searchResult = prefs.placeSearchResultAndroidAuto
|
||||
highlighedCharger = selectedCharger
|
||||
}
|
||||
|
||||
map.setMyLocationEnabled(true)
|
||||
map.uiSettings.setMyLocationButtonEnabled(false)
|
||||
map.setAttributionClickListener { attributions ->
|
||||
screenManager.push(MapAttributionScreen(carContext, attributions))
|
||||
}
|
||||
map.setOnMapClickListener {
|
||||
clearSelectedCharger()
|
||||
}
|
||||
|
||||
val mode = carContext.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
map.setMapStyle(
|
||||
if (mode == Configuration.UI_MODE_NIGHT_YES) AnyMap.Style.DARK else AnyMap.Style.NORMAL
|
||||
)
|
||||
|
||||
prefs.placeSearchResultAndroidAuto?.let { place ->
|
||||
// move to the location of the search result
|
||||
myLocationEnabled = false
|
||||
markerManager?.searchResult = place
|
||||
if (place.viewport != null) {
|
||||
map.moveCamera(map.cameraUpdateFactory.newLatLngBounds(place.viewport, 0))
|
||||
} else {
|
||||
map.moveCamera(map.cameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
|
||||
}
|
||||
} ?: if (prefs.currentMapMyLocationEnabled) {
|
||||
enableLocation(false)
|
||||
} else {
|
||||
// use position saved in preferences, fall back to default (Europe)
|
||||
val cameraUpdate =
|
||||
map.cameraUpdateFactory.newLatLngZoom(
|
||||
prefs.currentMapLocation,
|
||||
prefs.currentMapZoom
|
||||
)
|
||||
map.moveCamera(cameraUpdate)
|
||||
}
|
||||
|
||||
mapSurfaceCallback.cameraMoveStartedListener = {
|
||||
if (myLocationEnabled) {
|
||||
myLocationEnabled = false
|
||||
myLocationNeedsUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
mapSurfaceCallback.cameraIdleListener = {
|
||||
loadChargers()
|
||||
if (myLocationNeedsUpdate) {
|
||||
invalidate()
|
||||
myLocationNeedsUpdate = false
|
||||
}
|
||||
}
|
||||
loadChargers()
|
||||
}
|
||||
|
||||
private fun enableLocation(animated: Boolean) {
|
||||
myLocationEnabled = true
|
||||
myLocationNeedsUpdate = true
|
||||
if (location != null) {
|
||||
val map = map ?: return
|
||||
val update = map.cameraUpdateFactory.newLatLngZoom(
|
||||
LatLng.fromLocation(location),
|
||||
13f
|
||||
)
|
||||
if (animated) {
|
||||
mapSurfaceCallback.animateCamera(update)
|
||||
} else {
|
||||
map.moveCamera(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
284
app/src/main/java/net/vonforst/evmap/auto/MapSurfaceCallback.kt
Normal file
284
app/src/main/java/net/vonforst/evmap/auto/MapSurfaceCallback.kt
Normal file
@@ -0,0 +1,284 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.Presentation
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.hardware.display.VirtualDisplay
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.SurfaceCallback
|
||||
import androidx.car.app.SurfaceContainer
|
||||
import androidx.car.app.annotations.RequiresCarApi
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.AnyMap.CancelableCallback
|
||||
import com.car2go.maps.CameraUpdate
|
||||
import com.car2go.maps.MapContainerView
|
||||
import com.car2go.maps.MapFactory
|
||||
import com.car2go.maps.OnMapReadyCallback
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import kotlin.math.hypot
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
|
||||
class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCoroutineScope) :
|
||||
SurfaceCallback, OnMapReadyCallback {
|
||||
private val VIRTUAL_DISPLAY_NAME = "evmap_map"
|
||||
private val VELOCITY_THRESHOLD_IGNORE_FLING = 1000
|
||||
private val STATUSBAR_OFFSET_SYSTEMS = listOf(
|
||||
"VolvoCars/ihu_emulator_volvo_car/ihu_emulator:11",
|
||||
"Google/sdk_gcar_x86_64/generic_64bitonly_x86_64:11"
|
||||
)
|
||||
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
|
||||
private lateinit var virtualDisplay: VirtualDisplay
|
||||
lateinit var presentation: Presentation
|
||||
private lateinit var mapView: MapContainerView
|
||||
private var width: Int = 0
|
||||
private var height: Int = 0
|
||||
private var visibleArea: Rect? = null
|
||||
private var map: AnyMap? = null
|
||||
private val mapCallbacks = mutableListOf<OnMapReadyCallback>()
|
||||
|
||||
private var flingAnimator: ValueAnimator? = null
|
||||
private var idle = true
|
||||
private var idleDelay: Job? = null
|
||||
var cameraMoveStartedListener: (() -> Unit)? = null
|
||||
var cameraIdleListener: (() -> Unit)? = null
|
||||
|
||||
|
||||
override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
|
||||
if (surfaceContainer.surface == null || surfaceContainer.dpi == 0 || surfaceContainer.height == 0 || surfaceContainer.width == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Build.FINGERPRINT.contains("emulator") || Build.FINGERPRINT.contains("sdk_gcar")) {
|
||||
// fix for MapLibre in Android Automotive Emulators
|
||||
System.setProperty("ro.kernel.qemu", "1")
|
||||
}
|
||||
|
||||
width = surfaceContainer.width
|
||||
height = surfaceContainer.height
|
||||
virtualDisplay = ContextCompat
|
||||
.getSystemService(ctx, DisplayManager::class.java)!!
|
||||
.createVirtualDisplay(
|
||||
VIRTUAL_DISPLAY_NAME,
|
||||
width,
|
||||
height,
|
||||
(surfaceContainer.dpi * when (getMapProvider()) {
|
||||
"mapbox" -> 1.6
|
||||
"google" -> 1.0
|
||||
else -> 1.0
|
||||
}).roundToInt(),
|
||||
surfaceContainer.surface,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
|
||||
)
|
||||
presentation = Presentation(ctx, virtualDisplay.display, R.style.AppTheme)
|
||||
|
||||
mapView = createMap(presentation.context)
|
||||
mapView.onCreate(null)
|
||||
mapView.onResume()
|
||||
|
||||
presentation.setContentView(mapView)
|
||||
presentation.show()
|
||||
|
||||
mapView.getMapAsync(this)
|
||||
}
|
||||
|
||||
private fun getMapProvider(): String = if (BuildConfig.FLAVOR_automotive == "automotive") {
|
||||
// Google Maps SDK is not available on AAOS (not even AAOS with GAS, so far)
|
||||
"mapbox"
|
||||
} else prefs.mapProvider
|
||||
|
||||
override fun onVisibleAreaChanged(visibleArea: Rect) {
|
||||
Log.d("MapSurfaceCallback", "visible area: $visibleArea")
|
||||
this.visibleArea = visibleArea
|
||||
updateVisibleArea()
|
||||
}
|
||||
|
||||
override fun onStableAreaChanged(stableArea: Rect) {
|
||||
Log.d("MapSurfaceCallback", "stable area: $stableArea")
|
||||
}
|
||||
|
||||
override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
|
||||
mapView.onPause()
|
||||
mapView.onStop()
|
||||
mapView.onDestroy()
|
||||
map = null
|
||||
|
||||
presentation.dismiss()
|
||||
virtualDisplay.release()
|
||||
}
|
||||
|
||||
@RequiresCarApi(2)
|
||||
override fun onScroll(distanceX: Float, distanceY: Float) {
|
||||
flingAnimator?.cancel()
|
||||
val map = map ?: return
|
||||
map.moveCamera(map.cameraUpdateFactory.scrollBy(distanceX, distanceY))
|
||||
dispatchCameraMoveStarted()
|
||||
}
|
||||
|
||||
@RequiresCarApi(2)
|
||||
override fun onFling(velocityX: Float, velocityY: Float) {
|
||||
val map = map ?: return
|
||||
val screenDensity: Float = presentation.resources.displayMetrics.density
|
||||
|
||||
// calculate velocity vector for xy dimensions, independent from screen size
|
||||
val velocityXY =
|
||||
hypot((velocityX / screenDensity).toDouble(), (velocityY / screenDensity).toDouble())
|
||||
if (velocityXY < VELOCITY_THRESHOLD_IGNORE_FLING) {
|
||||
// ignore short flings, these can occur when other gestures just have finished executing
|
||||
return
|
||||
}
|
||||
|
||||
idleDelay?.cancel()
|
||||
|
||||
val offsetX = velocityX / 10
|
||||
val offsetY = velocityY / 10
|
||||
val animationTime = (velocityXY / 10).roundToLong()
|
||||
|
||||
flingAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = animationTime
|
||||
interpolator = LinearOutSlowInInterpolator()
|
||||
|
||||
var last = 0f
|
||||
addUpdateListener {
|
||||
val current = it.animatedFraction
|
||||
val diff = last - current
|
||||
map.moveCamera(map.cameraUpdateFactory.scrollBy(diff * offsetX, diff * offsetY))
|
||||
last = current
|
||||
}
|
||||
start()
|
||||
|
||||
doOnEnd { dispatchCameraIdle() }
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresCarApi(2)
|
||||
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
|
||||
flingAnimator?.cancel()
|
||||
val map = map ?: return
|
||||
if (scaleFactor == 2f) return
|
||||
|
||||
val offsetX = (focusX - mapView.width / 2) * (scaleFactor - 1f)
|
||||
val offsetY = (offsetY(focusY) - mapView.height / 2) * (scaleFactor - 1f)
|
||||
|
||||
Log.i("MapSurfaceCallback", "focus: $focusX, $focusY, scaleFactor: $scaleFactor")
|
||||
map.moveCamera(map.cameraUpdateFactory.zoomBy(scaleFactor - 1))
|
||||
map.moveCamera(map.cameraUpdateFactory.scrollBy(offsetX, offsetY))
|
||||
dispatchCameraMoveStarted()
|
||||
}
|
||||
|
||||
fun animateCamera(update: CameraUpdate) {
|
||||
val map = map ?: return
|
||||
map.animateCamera(update, object : CancelableCallback {
|
||||
override fun onFinish() {
|
||||
dispatchCameraIdle()
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun dispatchCameraMoveStarted() {
|
||||
if (idle) {
|
||||
idle = false
|
||||
cameraMoveStartedListener?.invoke()
|
||||
}
|
||||
idleDelay?.cancel()
|
||||
idleDelay = lifecycleScope.launch {
|
||||
delay(500)
|
||||
dispatchCameraIdle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchCameraIdle() {
|
||||
idle = true
|
||||
cameraIdleListener?.invoke()
|
||||
}
|
||||
|
||||
@RequiresCarApi(5)
|
||||
override fun onClick(x: Float, y: Float) {
|
||||
flingAnimator?.cancel()
|
||||
val downTime: Long = SystemClock.uptimeMillis()
|
||||
val eventTime: Long = downTime + 100
|
||||
val yOffset = offsetY(y)
|
||||
|
||||
val downEvent = MotionEvent.obtain(
|
||||
downTime,
|
||||
downTime,
|
||||
MotionEvent.ACTION_DOWN,
|
||||
x,
|
||||
yOffset,
|
||||
0
|
||||
)
|
||||
mapView.dispatchTouchEvent(downEvent)
|
||||
downEvent.recycle()
|
||||
val upEvent = MotionEvent.obtain(
|
||||
downTime,
|
||||
eventTime,
|
||||
MotionEvent.ACTION_UP,
|
||||
x,
|
||||
yOffset,
|
||||
0
|
||||
)
|
||||
mapView.dispatchTouchEvent(upEvent)
|
||||
upEvent.recycle()
|
||||
}
|
||||
|
||||
private fun offsetY(y: Float): Float {
|
||||
if (!STATUSBAR_OFFSET_SYSTEMS.any { Build.FINGERPRINT.startsWith(it) }) return y
|
||||
|
||||
// In some emulators, touch locations are offset by the status bar height
|
||||
// related: https://issuetracker.google.com/issues/256905247
|
||||
val resId = ctx.resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
val offset = resId.takeIf { it > 0 }?.let { ctx.resources.getDimensionPixelSize(it) } ?: 0
|
||||
return y + offset
|
||||
}
|
||||
|
||||
private fun createMap(ctx: Context): MapContainerView {
|
||||
val priority = arrayOf(
|
||||
when (getMapProvider()) {
|
||||
"mapbox" -> MapFactory.MAPLIBRE
|
||||
"google" -> MapFactory.GOOGLE
|
||||
else -> null
|
||||
},
|
||||
MapFactory.GOOGLE,
|
||||
MapFactory.MAPLIBRE
|
||||
)
|
||||
return MapFactory.createMap(ctx, priority).view
|
||||
}
|
||||
|
||||
override fun onMapReady(anyMap: AnyMap) {
|
||||
this.map = anyMap
|
||||
updateVisibleArea()
|
||||
mapCallbacks.forEach { it.onMapReady(anyMap) }
|
||||
mapCallbacks.clear()
|
||||
}
|
||||
|
||||
private fun updateVisibleArea() {
|
||||
visibleArea?.let {
|
||||
map?.setPadding(it.left, it.top, width - it.right, height - it.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
fun getMapAsync(callback: OnMapReadyCallback) {
|
||||
mapCallbacks.add(callback)
|
||||
}
|
||||
}
|
||||
@@ -11,19 +11,34 @@ 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.car.app.model.Action
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.DistanceSpan
|
||||
import androidx.car.app.model.ItemList
|
||||
import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.SearchTemplate
|
||||
import androidx.car.app.model.Template
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
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.autocomplete.ApiUnavailableException
|
||||
import net.vonforst.evmap.autocomplete.AutocompletePlace
|
||||
import net.vonforst.evmap.autocomplete.AutocompleteProvider
|
||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.autocomplete.getAutocompleteProviders
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.storage.RecentAutocompletePlace
|
||||
@@ -117,7 +132,7 @@ class PlaceSearchScreen(
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
val placeDetails = getDetails(place.id) ?: return@launch
|
||||
prefs.placeSearchResultAndroidAuto = placeDetails.latLng
|
||||
prefs.placeSearchResultAndroidAuto = placeDetails
|
||||
prefs.placeSearchResultAndroidAutoName =
|
||||
place.primaryText.toString()
|
||||
screenManager.popTo(MapScreen.MARKER)
|
||||
|
||||
@@ -43,6 +43,7 @@ import net.vonforst.evmap.api.availability.tesla.TeslaOwnerApi
|
||||
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.currencyDisplayName
|
||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
|
||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
|
||||
import net.vonforst.evmap.getPackageInfoCompat
|
||||
@@ -113,22 +114,25 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
}
|
||||
.build()
|
||||
)
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_chargers_ahead))
|
||||
.setToggle(Toggle.Builder {
|
||||
prefs.showChargersAheadAndroidAuto = it
|
||||
}.setChecked(prefs.showChargersAheadAndroidAuto).build())
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_navigation
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
if (carContext.carAppApiLevel < 7 || !carContext.isAppDrivenRefreshSupported) {
|
||||
// this option is only supported in LegacyMapScreen
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_chargers_ahead))
|
||||
.setToggle(Toggle.Builder {
|
||||
prefs.showChargersAheadAndroidAuto = it
|
||||
}.setChecked(prefs.showChargersAheadAndroidAuto).build())
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_navigation
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
addItem(
|
||||
Row.Builder()
|
||||
@@ -163,6 +167,10 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
carContext.resources.getStringArray(R.array.pref_search_provider_names)
|
||||
val searchProviderValues =
|
||||
carContext.resources.getStringArray(R.array.pref_search_provider_values)
|
||||
val mapProviderNames =
|
||||
carContext.resources.getStringArray(R.array.pref_map_provider_names)
|
||||
val mapProviderValues =
|
||||
carContext.resources.getStringArray(R.array.pref_map_provider_values)
|
||||
|
||||
var teslaLoggingIn = false
|
||||
|
||||
@@ -202,6 +210,25 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
if (supportsNewMapScreen(carContext) && BuildConfig.FLAVOR_automotive != "automotive") {
|
||||
// Google Maps SDK is not available on AAOS (not even AAOS with GAS, so far)
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_map_provider))
|
||||
setBrowsable(true)
|
||||
val mapProviderId = prefs.mapProvider
|
||||
val mapProviderDesc =
|
||||
mapProviderNames[mapProviderValues.indexOf(mapProviderId)]
|
||||
addText(mapProviderDesc)
|
||||
setOnClickListener {
|
||||
screenManager.push(
|
||||
ChooseDataSourceScreen(
|
||||
carContext,
|
||||
ChooseDataSourceScreen.Type.MAP_PROVIDER
|
||||
)
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_search_delete_recent))
|
||||
setOnClickListener {
|
||||
@@ -340,25 +367,33 @@ class ChooseDataSourceScreen(
|
||||
@StringRes val extraDesc: Int? = null
|
||||
) : Screen(ctx) {
|
||||
enum class Type {
|
||||
CHARGER_DATA_SOURCE, SEARCH_PROVIDER
|
||||
CHARGER_DATA_SOURCE, SEARCH_PROVIDER, MAP_PROVIDER
|
||||
}
|
||||
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
val title = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> R.string.pref_data_source
|
||||
Type.SEARCH_PROVIDER -> R.string.pref_search_provider
|
||||
Type.MAP_PROVIDER -> R.string.pref_map_provider
|
||||
}
|
||||
val names = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> carContext.resources.getStringArray(R.array.pref_data_source_names)
|
||||
Type.SEARCH_PROVIDER -> carContext.resources.getStringArray(R.array.pref_search_provider_names)
|
||||
}
|
||||
val values = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> carContext.resources.getStringArray(R.array.pref_data_source_values)
|
||||
Type.SEARCH_PROVIDER -> carContext.resources.getStringArray(R.array.pref_search_provider_values)
|
||||
}
|
||||
val names = carContext.resources.getStringArray(
|
||||
when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> R.array.pref_data_source_names
|
||||
Type.SEARCH_PROVIDER -> R.array.pref_search_provider_names
|
||||
Type.MAP_PROVIDER -> R.array.pref_map_provider_names
|
||||
}
|
||||
)
|
||||
val values = carContext.resources.getStringArray(
|
||||
when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> R.array.pref_data_source_values
|
||||
Type.SEARCH_PROVIDER -> R.array.pref_search_provider_values
|
||||
Type.MAP_PROVIDER -> R.array.pref_map_provider_values
|
||||
}
|
||||
)
|
||||
val currentValue: String = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> prefs.dataSource
|
||||
Type.SEARCH_PROVIDER -> prefs.searchProvider
|
||||
Type.MAP_PROVIDER -> prefs.mapProvider
|
||||
}
|
||||
val descriptions = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> listOf(
|
||||
@@ -366,6 +401,7 @@ class ChooseDataSourceScreen(
|
||||
carContext.getString(R.string.data_source_openchargemap_desc)
|
||||
)
|
||||
Type.SEARCH_PROVIDER -> null
|
||||
Type.MAP_PROVIDER -> null
|
||||
}
|
||||
val callback: (String) -> Unit = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> { it ->
|
||||
@@ -375,6 +411,9 @@ class ChooseDataSourceScreen(
|
||||
Type.SEARCH_PROVIDER -> { it ->
|
||||
prefs.searchProvider = it
|
||||
}
|
||||
Type.MAP_PROVIDER -> { it ->
|
||||
prefs.mapProvider = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -486,10 +525,9 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_currency))
|
||||
|
||||
val names =
|
||||
carContext.resources.getStringArray(R.array.pref_chargeprice_currency_names)
|
||||
val values =
|
||||
carContext.resources.getStringArray(R.array.pref_chargeprice_currency_values)
|
||||
carContext.resources.getStringArray(R.array.pref_chargeprice_currencies)
|
||||
val names = values.map(::currencyDisplayName)
|
||||
val index = values.indexOf(prefs.chargepriceCurrency)
|
||||
addText(if (index >= 0) names[index] else "")
|
||||
|
||||
@@ -629,8 +667,8 @@ class SelectCurrencyScreen(ctx: CarContext) : MultiSelectSearchScreen<Pair<Strin
|
||||
override fun getLabel(it: Pair<String, String>): String = it.first
|
||||
|
||||
override suspend fun loadData(): List<Pair<String, String>> {
|
||||
val names = carContext.resources.getStringArray(R.array.pref_chargeprice_currency_names)
|
||||
val values = carContext.resources.getStringArray(R.array.pref_chargeprice_currency_values)
|
||||
val values = carContext.resources.getStringArray(R.array.pref_chargeprice_currencies)
|
||||
val names = values.map(::currencyDisplayName)
|
||||
return names.zip(values)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,46 @@ import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.TextPaint
|
||||
import android.util.Log
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.HostException
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.hardware.common.CarUnit
|
||||
import androidx.car.app.model.*
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.CarIconSpan
|
||||
import androidx.car.app.model.Distance
|
||||
import androidx.car.app.model.ForegroundCarColorSpan
|
||||
import androidx.car.app.model.MessageTemplate
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.versioning.CarAppApiLevels
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.iconForPlugType
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.ftPerMile
|
||||
import net.vonforst.evmap.getPackageInfoCompat
|
||||
import net.vonforst.evmap.kmPerMile
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.shouldUseImperialUnits
|
||||
import net.vonforst.evmap.ui.availabilityText
|
||||
import net.vonforst.evmap.ydPerMile
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
|
||||
@@ -207,7 +224,9 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
|
||||
val version = getAndroidAutoVersion(ctx)
|
||||
// Android Auto 6.7 is required. 6.6 reports supporting API Level 3,
|
||||
// but crashes when using it. See: https://issuetracker.google.com/issues/199509584
|
||||
if (version[0] < "6" || version[0] == "6" && version[1] < "7") {
|
||||
val major = version[0].toIntOrNull() ?: return false
|
||||
val minor = version[1].toIntOrNull() ?: return false
|
||||
if (major < 6 || major < 6 && minor < 7) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -215,6 +234,9 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
fun supportsNewMapScreen(ctx: CarContext) =
|
||||
ctx.carAppApiLevel >= 7 && ctx.isAppDrivenRefreshSupported
|
||||
|
||||
fun openUrl(carContext: CarContext, url: String) {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
@@ -249,6 +271,54 @@ fun openUrl(carContext: CarContext, url: String) {
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToCharger(ctx: CarContext, charger: ChargeLocation) {
|
||||
val success = navigateCarApp(ctx, charger)
|
||||
if (!success && BuildConfig.FLAVOR_automotive == "automotive") {
|
||||
// on AAOS, some OEMs' navigation apps might not support
|
||||
navigateRegularApp(ctx, charger)
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateCarApp(ctx: CarContext, charger: ChargeLocation): Boolean {
|
||||
val coord = charger.coordinates
|
||||
val intent =
|
||||
Intent(
|
||||
CarContext.ACTION_NAVIGATE,
|
||||
Uri.parse("geo:${coord.lat},${coord.lng}")
|
||||
)
|
||||
try {
|
||||
ctx.startCarApp(intent)
|
||||
return true
|
||||
} catch (e: HostException) {
|
||||
Log.w("navigateToCharger", "Could not start navigation using car app intent")
|
||||
Log.w("navigateToCharger", intent.toString())
|
||||
e.printStackTrace()
|
||||
} catch (e: SecurityException) {
|
||||
Log.w("navigateToCharger", "Could not start navigation using car app intent")
|
||||
Log.w("navigateToCharger", intent.toString())
|
||||
e.printStackTrace()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun navigateRegularApp(ctx: CarContext, charger: ChargeLocation): Boolean {
|
||||
val coord = charger.coordinates
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(
|
||||
"geo:${coord.lat},${coord.lng}?q=${coord.lat},${coord.lng}(${
|
||||
Uri.encode(charger.name)
|
||||
})"
|
||||
)
|
||||
if (intent.resolveActivity(ctx.packageManager) != null) {
|
||||
ctx.startActivity(intent)
|
||||
return true
|
||||
} else {
|
||||
Log.w("navigateToCharger", "Could not start navigation using regular intent")
|
||||
Log.w("navigateToCharger", intent.toString())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
class DummyReturnScreen(ctx: CarContext) : Screen(ctx) {
|
||||
/*
|
||||
Dummy screen to get around template refresh limitations.
|
||||
@@ -273,4 +343,49 @@ class TextMeasurer(ctx: CarContext) {
|
||||
fun measureText(text: CharSequence): Float {
|
||||
return textPaint.measureText(text, 0, text.length)
|
||||
}
|
||||
}
|
||||
|
||||
fun generateChargepointsText(
|
||||
charger: ChargeLocation,
|
||||
availability: ChargeLocationStatus?,
|
||||
ctx: Context
|
||||
): SpannableStringBuilder {
|
||||
val chargepointsText = SpannableStringBuilder()
|
||||
charger.chargepointsMerged.forEachIndexed { i, cp ->
|
||||
chargepointsText.apply {
|
||||
if (i > 0) append(" · ")
|
||||
append("${cp.count}× ")
|
||||
val plugIcon = iconForPlugType(cp.type)
|
||||
if (plugIcon != 0) {
|
||||
append(
|
||||
nameForPlugType(ctx.stringProvider(), cp.type),
|
||||
CarIconSpan.create(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
ctx,
|
||||
plugIcon
|
||||
)
|
||||
).setTint(
|
||||
CarColor.createCustom(Color.WHITE, Color.BLACK)
|
||||
).build()
|
||||
),
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
} else {
|
||||
append(nameForPlugType(ctx.stringProvider(), cp.type))
|
||||
}
|
||||
cp.formatPower()?.let {
|
||||
append(" ")
|
||||
append(it)
|
||||
}
|
||||
}
|
||||
availability?.status?.get(cp)?.let { status ->
|
||||
chargepointsText.append(
|
||||
" (${availabilityText(status)}/${cp.count})",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
return chargepointsText
|
||||
}
|
||||
@@ -6,9 +6,18 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.info.*
|
||||
import androidx.car.app.model.*
|
||||
import androidx.car.app.hardware.info.CarSensors
|
||||
import androidx.car.app.hardware.info.Compass
|
||||
import androidx.car.app.hardware.info.EnergyLevel
|
||||
import androidx.car.app.hardware.info.Model
|
||||
import androidx.car.app.hardware.info.Speed
|
||||
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.Template
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
@@ -18,14 +27,14 @@ import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.CompassNeedle
|
||||
import net.vonforst.evmap.ui.Gauge
|
||||
import net.vonforst.evmap.utils.formatDecimal
|
||||
import patchedCarInfo
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class VehicleDataScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx),
|
||||
LocationAwareScreen, DefaultLifecycleObserver {
|
||||
private val carInfo =
|
||||
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
|
||||
private val carInfo = carContext.patchedCarInfo
|
||||
private val carSensors = carContext.patchedCarSensors
|
||||
private var model: Model? = null
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.mapbox.api.geocoding.v5.models.CarmenFeature
|
||||
import com.mapbox.geojson.BoundingBox
|
||||
import com.mapbox.geojson.Point
|
||||
import net.vonforst.evmap.R
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
|
||||
@@ -25,7 +26,7 @@ class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
|
||||
override val id = "mapbox"
|
||||
|
||||
override fun autocomplete(query: String, location: LatLng?): List<AutocompletePlace> {
|
||||
val result = MapboxGeocoding.builder().apply {
|
||||
val request = MapboxGeocoding.builder().apply {
|
||||
location?.let {
|
||||
proximity(Point.fromLngLat(location.longitude, location.latitude))
|
||||
}
|
||||
@@ -33,7 +34,12 @@ class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
|
||||
accessToken(context.getString(R.string.mapbox_key))
|
||||
autocomplete(true)
|
||||
this.query(query)
|
||||
}.build().executeCall()
|
||||
}
|
||||
val result = try {
|
||||
request.build().executeCall()
|
||||
} catch (e: HttpException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
if (!result.isSuccessful) {
|
||||
throw IOException(result.message())
|
||||
}
|
||||
@@ -113,8 +119,7 @@ class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
|
||||
|
||||
override fun getAttributionString(): Int = R.string.powered_by_mapbox
|
||||
|
||||
override fun getAttributionImage(dark: Boolean): Int =
|
||||
if (dark) com.mapbox.mapboxsdk.R.drawable.mapbox_logo_icon else R.drawable.mapbox_logo
|
||||
override fun getAttributionImage(dark: Boolean): Int = R.drawable.mapbox_logo
|
||||
}
|
||||
|
||||
private fun BoundingBox.toLatLngBounds(): LatLngBounds {
|
||||
|
||||
@@ -55,12 +55,10 @@ import androidx.transition.TransitionManager
|
||||
import coil.load
|
||||
import coil.memory.MemoryCache
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.MapFactory
|
||||
import com.car2go.maps.MapFragment
|
||||
import com.car2go.maps.OnMapReadyCallback
|
||||
import com.car2go.maps.model.BitmapDescriptor
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.Marker
|
||||
import com.car2go.maps.model.MarkerOptions
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.transition.MaterialArcMotion
|
||||
@@ -79,10 +77,7 @@ import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
|
||||
import com.stfalcon.imageviewer.StfalconImageViewer
|
||||
import io.michaelrocks.bimap.HashBiMap
|
||||
import io.michaelrocks.bimap.MutableBiMap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.ConnectorAdapter
|
||||
@@ -98,7 +93,6 @@ import net.vonforst.evmap.location.FusionEngine
|
||||
import net.vonforst.evmap.location.LocationEngine
|
||||
import net.vonforst.evmap.location.Priority
|
||||
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
|
||||
@@ -107,14 +101,8 @@ 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.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.MarkerManager
|
||||
import net.vonforst.evmap.ui.setTouchModal
|
||||
import net.vonforst.evmap.utils.boundingBox
|
||||
import net.vonforst.evmap.utils.checkAnyLocationPermission
|
||||
@@ -129,9 +117,6 @@ import net.vonforst.evmap.viewmodel.Status
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.contains
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -142,24 +127,16 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
private val galleryVm: GalleryViewModel by activityViewModels()
|
||||
private var mapFragment: MapFragment? = null
|
||||
private var map: AnyMap? = null
|
||||
private var markerManager: MarkerManager? = null
|
||||
private lateinit var locationEngine: LocationEngine
|
||||
private var requestingLocationUpdates = false
|
||||
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
|
||||
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
|
||||
private lateinit var detailsDialog: ConnectorDetailsDialog
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
|
||||
private var clusterMarkers: List<Marker> = emptyList()
|
||||
private var searchResultMarker: Marker? = null
|
||||
private var searchResultIcon: BitmapDescriptor? = null
|
||||
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
|
||||
private lateinit var animator: MarkerAnimator
|
||||
private lateinit var favToggle: MenuItem
|
||||
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
@@ -198,7 +175,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
prefs = PreferenceDataSource(requireContext())
|
||||
|
||||
locationEngine = FusionEngine(requireContext())
|
||||
clusterIconGenerator = ClusterIconGenerator(requireContext())
|
||||
|
||||
enterTransition = MaterialFadeThrough()
|
||||
exitTransition = MaterialFadeThrough()
|
||||
@@ -225,24 +201,20 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
mapFragment = MapFragment()
|
||||
mapFragment!!.priority = arrayOf(
|
||||
when (provider) {
|
||||
"mapbox" -> MapFragment.MAPBOX
|
||||
"google" -> MapFragment.GOOGLE
|
||||
"mapbox" -> MapFactory.MAPLIBRE
|
||||
"google" -> MapFactory.GOOGLE
|
||||
else -> null
|
||||
},
|
||||
MapFragment.GOOGLE,
|
||||
MapFragment.MAPBOX
|
||||
MapFactory.GOOGLE,
|
||||
MapFactory.MAPLIBRE
|
||||
)
|
||||
childFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.map, mapFragment!!, mapFragmentTag)
|
||||
.commit()
|
||||
|
||||
// reset map-related stuff (map provider may have changed)
|
||||
map = null
|
||||
markers.clear()
|
||||
clusterMarkers = emptyList()
|
||||
searchResultMarker = null
|
||||
searchResultIcon = null
|
||||
markerManager = null
|
||||
}
|
||||
|
||||
binding.detailAppBar.toolbar.popupTheme =
|
||||
@@ -275,7 +247,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
// 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
|
||||
// if we actually use map.setPadding here, MapLibre will re-trigger onApplyWindowInsets
|
||||
// and cause an infinite loop. So we rely on onMapReady being called later than
|
||||
// onApplyWindowInsets.
|
||||
|
||||
@@ -297,6 +269,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
get() = resources.getBoolean(R.bool.bottom_sheet_collapsible)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
if (!prefs.welcomeDialogShown || !prefs.dataSourceSet || !prefs.privacyAccepted) {
|
||||
findNavController().navigate(R.id.onboarding)
|
||||
}
|
||||
|
||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
|
||||
mapFragment!!.getMapAsync(this)
|
||||
@@ -628,16 +604,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
} else {
|
||||
vm.insertFavorite(charger)
|
||||
}
|
||||
markers.inverse[charger]?.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||
highlight = true,
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti(vm.filteredConnectors.value),
|
||||
fav = fav == null,
|
||||
mini = vm.useMiniMarkers.value == true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
@@ -689,16 +655,17 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.fabDirections.show()
|
||||
detailAppBarBehavior.setToolbarTitle(it.name)
|
||||
updateFavoriteToggle()
|
||||
highlightMarker(it)
|
||||
markerManager?.highlighedCharger = it
|
||||
markerManager?.animateBounce(it)
|
||||
} else {
|
||||
bottomSheetBehavior.state = STATE_HIDDEN
|
||||
unhighlightAllMarkers()
|
||||
markerManager?.highlighedCharger = null
|
||||
}
|
||||
}
|
||||
vm.chargepoints.observe(viewLifecycleOwner, Observer { res ->
|
||||
val chargepoints = res.data
|
||||
if (chargepoints != null) {
|
||||
updateMap(chargepoints)
|
||||
markerManager?.chargepoints = chargepoints
|
||||
}
|
||||
when (res.status) {
|
||||
Status.ERROR -> {
|
||||
@@ -721,10 +688,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
})
|
||||
vm.useMiniMarkers.observe(viewLifecycleOwner) {
|
||||
vm.chargepoints.value?.data?.let { updateMap(it) }
|
||||
markerManager?.mini = it
|
||||
}
|
||||
vm.filteredConnectors.observe(viewLifecycleOwner) {
|
||||
markerManager?.filteredConnectors = it
|
||||
}
|
||||
vm.favorites.observe(viewLifecycleOwner) {
|
||||
updateFavoriteToggle()
|
||||
markerManager?.favorites = it.map { it.favorite.chargerId }.toSet()
|
||||
}
|
||||
vm.searchResult.observe(viewLifecycleOwner) { place ->
|
||||
displaySearchResult(place, moveCamera = true)
|
||||
@@ -755,8 +726,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
private fun displaySearchResult(place: PlaceWithBounds?, moveCamera: Boolean) {
|
||||
val map = this.map ?: return
|
||||
searchResultMarker?.remove()
|
||||
searchResultMarker = null
|
||||
markerManager?.searchResult = place
|
||||
|
||||
if (place != null) {
|
||||
// disable location following when search result is shown
|
||||
@@ -768,18 +738,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
map.animateCamera(map.cameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
|
||||
}
|
||||
}
|
||||
|
||||
if (searchResultIcon == null) {
|
||||
searchResultIcon =
|
||||
map.bitmapDescriptorFactory.fromResource(R.drawable.ic_map_marker)
|
||||
}
|
||||
searchResultMarker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.z(placeSearchZ)
|
||||
.position(place.latLng)
|
||||
.icon(searchResultIcon)
|
||||
.anchor(0.5f, 1f)
|
||||
)
|
||||
} else {
|
||||
binding.search.setText("")
|
||||
}
|
||||
@@ -796,53 +754,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|| vm.selectedChargepoint.value != null
|
||||
}
|
||||
|
||||
private fun unhighlightAllMarkers() {
|
||||
markers.forEach { (m, c) ->
|
||||
m.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(c, vm.filteredConnectors.value),
|
||||
highlight = false,
|
||||
fault = c.faultReport != null,
|
||||
multi = c.isMulti(vm.filteredConnectors.value),
|
||||
fav = c.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
|
||||
mini = vm.useMiniMarkers.value == true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun highlightMarker(charger: ChargeLocation) {
|
||||
val marker = markers.inverse[charger] ?: return
|
||||
// highlight this marker
|
||||
marker.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||
highlight = true,
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti(vm.filteredConnectors.value),
|
||||
fav = charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
|
||||
mini = vm.useMiniMarkers.value == true
|
||||
)
|
||||
)
|
||||
animator.animateMarkerBounce(marker, vm.useMiniMarkers.value == true)
|
||||
|
||||
// un-highlight all other markers
|
||||
markers.forEach { (m, c) ->
|
||||
if (m != marker) {
|
||||
m.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(c, vm.filteredConnectors.value),
|
||||
highlight = false,
|
||||
fault = c.faultReport != null,
|
||||
multi = c.isMulti(vm.filteredConnectors.value),
|
||||
fav = c.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
|
||||
mini = vm.useMiniMarkers.value == true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFavoriteToggle() {
|
||||
val favs = vm.favorites.value ?: return
|
||||
val charger = vm.chargerSparse.value ?: return
|
||||
@@ -864,6 +775,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
if (photo == photos[position] && imageCacheKey != null) {
|
||||
placeholderMemoryCacheKey(imageCacheKey)
|
||||
}
|
||||
allowHardware(false)
|
||||
}
|
||||
}
|
||||
.withTransitionFrom(view as ImageView)
|
||||
@@ -1050,23 +962,26 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
this.map = map
|
||||
vm.mapProjection = map.projection
|
||||
val context = this.context ?: return
|
||||
chargerIconGenerator = ChargerIconGenerator(context, map.bitmapDescriptorFactory)
|
||||
|
||||
if (BuildConfig.FLAVOR.contains("google") && mapFragment!!.priority[0] == MapFragment.GOOGLE) {
|
||||
// Google Maps: icons can be generated in background thread
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
chargerIconGenerator.preloadCache()
|
||||
}
|
||||
view ?: return
|
||||
markerManager = MarkerManager(context, map, this).apply {
|
||||
onChargerClick = {
|
||||
vm.chargerSparse.value = it
|
||||
}
|
||||
} else {
|
||||
// Mapbox: needs to be run on main thread
|
||||
chargerIconGenerator.preloadCache()
|
||||
onClusterClick = {
|
||||
val newZoom = map.cameraPosition.zoom + 2
|
||||
map.animateCamera(
|
||||
map.cameraUpdateFactory.newLatLngZoom(
|
||||
LatLng(it.coordinates.lat, it.coordinates.lng),
|
||||
newZoom
|
||||
)
|
||||
)
|
||||
}
|
||||
chargepoints = vm.chargepoints.value?.data ?: emptyList()
|
||||
highlighedCharger = vm.chargerSparse.value
|
||||
searchResult = vm.searchResult.value
|
||||
favorites = vm.favorites.value?.map { it.favorite.chargerId }?.toSet() ?: emptySet()
|
||||
}
|
||||
|
||||
|
||||
|
||||
animator = MarkerAnimator(chargerIconGenerator)
|
||||
map.uiSettings.setTiltGesturesEnabled(false)
|
||||
map.uiSettings.setRotateGesturesEnabled(prefs.mapRotateGesturesEnabled)
|
||||
map.setIndoorEnabled(false)
|
||||
@@ -1119,26 +1034,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
}
|
||||
map.setOnMarkerClickListener { marker ->
|
||||
when (marker) {
|
||||
in markers -> {
|
||||
vm.chargerSparse.value = markers[marker]
|
||||
true
|
||||
}
|
||||
in clusterMarkers -> {
|
||||
val newZoom = map.cameraPosition.zoom + 2
|
||||
map.animateCamera(
|
||||
map.cameraUpdateFactory.newLatLngZoom(
|
||||
marker.position,
|
||||
newZoom
|
||||
)
|
||||
)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
}
|
||||
map.setOnMapClickListener {
|
||||
if (backPressedCallback.isEnabled) {
|
||||
backPressedCallback.handleOnBackPressed()
|
||||
@@ -1270,111 +1165,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun updateMap(chargepoints: List<ChargepointListItem>) {
|
||||
val map = this.map ?: return
|
||||
clusterMarkers.forEach { it.remove() }
|
||||
|
||||
val clusters = chargepoints.filterIsInstance<ChargeLocationCluster>()
|
||||
val chargers = chargepoints.filterIsInstance<ChargeLocation>()
|
||||
|
||||
val chargepointIds = chargers.map { it.id }.toSet()
|
||||
|
||||
// update icons of existing markers (connector filter may have changed)
|
||||
for ((marker, charger) in markers) {
|
||||
val highlight = charger.id == vm.chargerSparse.value?.id
|
||||
marker.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||
highlight = highlight,
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti(vm.filteredConnectors.value),
|
||||
fav = charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
|
||||
mini = vm.useMiniMarkers.value == true
|
||||
)
|
||||
)
|
||||
marker.setAnchor(0.5f, if (vm.useMiniMarkers.value == true) 0.5f else 1f)
|
||||
}
|
||||
|
||||
if (chargers.toSet() != markers.values) {
|
||||
// remove markers that disappeared
|
||||
val bounds = map.projection.visibleRegion.latLngBounds
|
||||
markers.entries.toList().forEach {
|
||||
val marker = it.key
|
||||
val charger = it.value
|
||||
if (!chargepointIds.contains(charger.id)) {
|
||||
// animate marker if it is visible, otherwise remove immediately
|
||||
if (bounds.contains(marker.position)) {
|
||||
val tint = getMarkerTint(charger, vm.filteredConnectors.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())
|
||||
animator.animateMarkerDisappear(
|
||||
marker, tint, highlight, fault, multi, fav,
|
||||
vm.useMiniMarkers.value == true
|
||||
)
|
||||
} else {
|
||||
animator.deleteMarker(marker)
|
||||
}
|
||||
markers.remove(marker)
|
||||
}
|
||||
}
|
||||
// add new markers
|
||||
val map1 = markers.values.map { it.id }
|
||||
for (charger in chargers) {
|
||||
if (!map1.contains(charger.id)) {
|
||||
val tint = getMarkerTint(charger, vm.filteredConnectors.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())
|
||||
val marker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
|
||||
.z(chargerZ)
|
||||
.icon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
tint,
|
||||
0f,
|
||||
255,
|
||||
highlight,
|
||||
fault,
|
||||
multi,
|
||||
fav,
|
||||
vm.useMiniMarkers.value == true
|
||||
)
|
||||
)
|
||||
.anchor(0.5f, if (vm.useMiniMarkers.value == true) 0.5f else 1f)
|
||||
)
|
||||
animator.animateMarkerAppear(
|
||||
marker, tint, highlight, fault, multi, fav,
|
||||
vm.useMiniMarkers.value == true
|
||||
)
|
||||
markers[marker] = charger
|
||||
}
|
||||
}
|
||||
previousChargepointIds = chargepointIds
|
||||
}
|
||||
clusterMarkers = clusters.map { cluster ->
|
||||
map.addMarker(
|
||||
MarkerOptions()
|
||||
.position(LatLng(cluster.coordinates.lat, cluster.coordinates.lng))
|
||||
.z(clusterZ)
|
||||
.icon(
|
||||
map.bitmapDescriptorFactory.fromBitmap(
|
||||
clusterIconGenerator.makeIcon(
|
||||
cluster.clusterCount.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
.anchor(0.5f, 0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.map, menu)
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ import android.text.style.RelativeSizeSpan
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.ListPreference
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.currencyDisplayName
|
||||
import net.vonforst.evmap.ui.MultiSelectDialogPreference
|
||||
import net.vonforst.evmap.viewmodel.SettingsViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
@@ -73,6 +75,11 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
|
||||
}
|
||||
}
|
||||
updateNativeIntegrationState()
|
||||
|
||||
val currencyPreference = findPreference<ListPreference>("chargeprice_currency")!!
|
||||
currencyPreference.entries = currencyPreference.entryValues.map {
|
||||
currencyDisplayName(it.toString()).replaceFirstChar { it.uppercase() }
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
private fun updateNativeIntegrationState() {
|
||||
|
||||
@@ -9,9 +9,11 @@ import android.provider.Settings
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import com.github.erfansn.localeconfigx.configuredLocales
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.isAppInstalled
|
||||
import net.vonforst.evmap.ui.getAppLocale
|
||||
import net.vonforst.evmap.ui.map
|
||||
import net.vonforst.evmap.ui.updateAppLocale
|
||||
import net.vonforst.evmap.ui.updateNightMode
|
||||
|
||||
@@ -23,11 +25,7 @@ class UiSettingsFragment : BaseSettingsFragment() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings_ui, rootKey)
|
||||
|
||||
langPref = findPreference("language")!!
|
||||
langPref.setOnPreferenceChangeListener { _, newValue ->
|
||||
updateAppLocale(newValue as String)
|
||||
true
|
||||
}
|
||||
setupLangPref()
|
||||
|
||||
val appLinkPref = findPreference<Preference>("applink_associate")!!
|
||||
appLinkPref.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
@@ -36,6 +34,31 @@ class UiSettingsFragment : BaseSettingsFragment() {
|
||||
immediateNavPref.isVisible = isGoogleMapsInstalled()
|
||||
}
|
||||
|
||||
private fun setupLangPref() {
|
||||
langPref = findPreference("language")!!
|
||||
val configuredLocales = requireContext().configuredLocales
|
||||
val numLocalesByLang = configuredLocales.map { it.language }.groupingBy { it }.eachCount()
|
||||
|
||||
val localeNames = configuredLocales.map {
|
||||
val name = if (numLocalesByLang[it.language]!! > 1) {
|
||||
it.getDisplayName(it)
|
||||
} else {
|
||||
it.getDisplayLanguage(it)
|
||||
}
|
||||
name.replaceFirstChar { c -> c.uppercase(it) }
|
||||
}
|
||||
val localeTags = configuredLocales.map { it.toLanguageTag() }
|
||||
|
||||
langPref.entries =
|
||||
(listOf(getString(R.string.pref_language_device_default)) + localeNames).toTypedArray()
|
||||
langPref.entryValues =
|
||||
(listOf("default") + localeTags).toTypedArray()
|
||||
langPref.setOnPreferenceChangeListener { _, newValue ->
|
||||
updateAppLocale(newValue as String)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun isGoogleMapsInstalled() =
|
||||
requireContext().packageManager.isAppInstalled("com.google.android.apps.maps")
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import android.content.SharedPreferences.Editor
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import java.time.Instant
|
||||
@@ -253,10 +255,16 @@ class PreferenceDataSource(val context: Context) {
|
||||
.apply()
|
||||
}
|
||||
|
||||
var placeSearchResultAndroidAuto: LatLng?
|
||||
get() = sp.getLatLng("place_search_result_android_auto")
|
||||
var placeSearchResultAndroidAuto: PlaceWithBounds?
|
||||
get() {
|
||||
val latLng = sp.getLatLng("place_search_result_android_auto")
|
||||
val bounds = sp.getLatLngBounds("place_search_result_android_auto_viewport")
|
||||
return latLng?.let { PlaceWithBounds(latLng, bounds) }
|
||||
}
|
||||
set(value) {
|
||||
sp.edit().putLatLng("place_search_result_android_auto", value).apply()
|
||||
sp.edit().putLatLng("place_search_result_android_auto", value?.latLng).apply()
|
||||
sp.edit().putLatLngBounds("place_search_result_android_auto_viewport", value?.viewport)
|
||||
.apply()
|
||||
}
|
||||
|
||||
var placeSearchResultAndroidAutoName: String?
|
||||
@@ -318,7 +326,7 @@ class PreferenceDataSource(val context: Context) {
|
||||
}
|
||||
|
||||
fun SharedPreferences.getLatLng(key: String): LatLng? =
|
||||
if (contains("${key}_lat") && contains("${key}_lng")) {
|
||||
if (containsLatLng(key)) {
|
||||
LatLng(
|
||||
Double.fromBits(getLong("${key}_lat", 0L)),
|
||||
Double.fromBits(getLong("${key}_lng", 0L))
|
||||
@@ -335,3 +343,23 @@ fun Editor.putLatLng(key: String, value: LatLng?): Editor {
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun SharedPreferences.containsLatLng(key: String) = contains("${key}_lat") && contains("${key}_lng")
|
||||
|
||||
fun SharedPreferences.getLatLngBounds(key: String): LatLngBounds? =
|
||||
if (containsLatLng("${key}_sw") && containsLatLng("${key}_ne")) {
|
||||
LatLngBounds(
|
||||
getLatLng("${key}_sw"), getLatLng("${key}_ne")
|
||||
)
|
||||
} else null
|
||||
|
||||
fun Editor.putLatLngBounds(key: String, value: LatLngBounds?): Editor {
|
||||
if (value == null) {
|
||||
putLatLng("${key}_sw", null)
|
||||
putLatLng("${key}_ne", null)
|
||||
} else {
|
||||
putLatLng("${key}_sw", value.southwest)
|
||||
putLatLng("${key}_ne", value.northeast)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ package net.vonforst.evmap.ui
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import net.vonforst.evmap.R
|
||||
import com.github.erfansn.localeconfigx.configuredLocales
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
fun updateNightMode(prefs: PreferenceDataSource) {
|
||||
@@ -33,8 +34,11 @@ fun getAppLocale(context: Context): String? {
|
||||
"default"
|
||||
} else {
|
||||
val arr = Array(locales.size()) { locales.get(it)!!.toLanguageTag() }
|
||||
val choices =
|
||||
context.resources.getStringArray(R.array.pref_language_values).joinToString(",")
|
||||
LocaleListCompat.forLanguageTags(choices).getFirstMatch(arr)?.toLanguageTag()
|
||||
val choices = context.configuredLocales
|
||||
choices.getFirstMatch(arr)?.toLanguageTag()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <R> LocaleListCompat.map(transform: (Locale) -> R): List<R> = List(size()) {
|
||||
transform(get(it)!!)
|
||||
}
|
||||
@@ -1,13 +1,29 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.view.animation.BounceInterpolator
|
||||
import androidx.core.animation.addListener
|
||||
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
|
||||
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.Marker
|
||||
import com.car2go.maps.model.MarkerOptions
|
||||
import io.michaelrocks.bimap.HashBiMap
|
||||
import io.michaelrocks.bimap.MutableBiMap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.ChargeLocationCluster
|
||||
import net.vonforst.evmap.model.ChargepointListItem
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import kotlin.math.max
|
||||
|
||||
fun getMarkerTint(
|
||||
@@ -29,6 +45,217 @@ val chargerZ = 1
|
||||
val clusterZ = chargerZ + 1
|
||||
val placeSearchZ = clusterZ + 1
|
||||
|
||||
class MarkerManager(
|
||||
val context: Context,
|
||||
val map: AnyMap,
|
||||
val lifecycle: LifecycleOwner,
|
||||
markerHeight: Int = 48
|
||||
) {
|
||||
private val clusterIconGenerator = ClusterIconGenerator(context)
|
||||
private val chargerIconGenerator =
|
||||
ChargerIconGenerator(context, map.bitmapDescriptorFactory, height = markerHeight)
|
||||
private val prefs = PreferenceDataSource(context)
|
||||
private val animator = MarkerAnimator(chargerIconGenerator)
|
||||
|
||||
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
|
||||
private var clusterMarkers: MutableBiMap<Marker, ChargeLocationCluster> = HashBiMap()
|
||||
private var searchResultMarker: Marker? = null
|
||||
private var searchResultIcon =
|
||||
map.bitmapDescriptorFactory.fromResource(R.drawable.ic_map_marker)
|
||||
|
||||
var mini = false
|
||||
var filteredConnectors: Set<String>? = null
|
||||
var onChargerClick: ((ChargeLocation) -> Unit)? = null
|
||||
var onClusterClick: ((ChargeLocationCluster) -> Unit)? = null
|
||||
|
||||
var chargepoints: List<ChargepointListItem> = emptyList()
|
||||
@Synchronized set(value) {
|
||||
field = value
|
||||
updateChargepoints()
|
||||
}
|
||||
|
||||
var highlighedCharger: ChargeLocation? = null
|
||||
set(value) {
|
||||
field = value
|
||||
updateChargerIcons()
|
||||
}
|
||||
|
||||
var searchResult: PlaceWithBounds? = null
|
||||
set(value) {
|
||||
field = value
|
||||
updateSearchResultMarker()
|
||||
}
|
||||
|
||||
var favorites: Set<Long> = emptySet()
|
||||
set(value) {
|
||||
field = value
|
||||
updateChargerIcons()
|
||||
}
|
||||
|
||||
init {
|
||||
map.setOnMarkerClickListener { marker ->
|
||||
when (marker) {
|
||||
in markers -> {
|
||||
val charger = markers[marker] ?: return@setOnMarkerClickListener false
|
||||
onChargerClick?.invoke(charger)
|
||||
true
|
||||
}
|
||||
|
||||
in clusterMarkers -> {
|
||||
val cluster = clusterMarkers[marker] ?: return@setOnMarkerClickListener false
|
||||
onClusterClick?.invoke(cluster)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
if (BuildConfig.FLAVOR.contains("google") && prefs.mapProvider == "google") {
|
||||
// Google Maps: icons can be generated in background thread
|
||||
lifecycle.lifecycleScope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
chargerIconGenerator.preloadCache()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// MapLibre: needs to be run on main thread
|
||||
chargerIconGenerator.preloadCache()
|
||||
}
|
||||
}
|
||||
|
||||
fun animateBounce(charger: ChargeLocation) {
|
||||
val marker = markers.inverse[charger] ?: return
|
||||
animator.animateMarkerBounce(marker, mini)
|
||||
}
|
||||
|
||||
private fun updateSearchResultMarker() {
|
||||
searchResultMarker?.remove()
|
||||
searchResultMarker = null
|
||||
|
||||
searchResult?.let {
|
||||
searchResultMarker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.z(placeSearchZ)
|
||||
.position(it.latLng)
|
||||
.icon(searchResultIcon)
|
||||
.anchor(0.5f, 1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateChargepoints() {
|
||||
val clusters = chargepoints.filterIsInstance<ChargeLocationCluster>()
|
||||
val chargers = chargepoints.filterIsInstance<ChargeLocation>()
|
||||
|
||||
val chargepointIds = chargers.map { it.id }.toSet()
|
||||
|
||||
// update icons of existing markers (connector filter may have changed)
|
||||
updateChargerIcons()
|
||||
|
||||
if (chargers.toSet() != markers.values) {
|
||||
// remove markers that disappeared
|
||||
val bounds = map.projection.visibleRegion.latLngBounds
|
||||
markers.entries.toList().forEach { (marker, charger) ->
|
||||
if (!chargepointIds.contains(charger.id)) {
|
||||
// animate marker if it is visible, otherwise remove immediately
|
||||
if (bounds.contains(marker.position)) {
|
||||
animateMarker(charger, marker, false)
|
||||
} else {
|
||||
animator.deleteMarker(marker)
|
||||
}
|
||||
markers.remove(marker)
|
||||
}
|
||||
}
|
||||
// add new markers
|
||||
val map1 = markers.values.map { it.id }
|
||||
for (charger in chargers) {
|
||||
if (!map1.contains(charger.id)) {
|
||||
val marker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
|
||||
.z(chargerZ)
|
||||
.icon(makeIcon(charger))
|
||||
.anchor(0.5f, if (mini) 0.5f else 1f)
|
||||
)
|
||||
animateMarker(charger, marker, true)
|
||||
markers[marker] = charger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clusters.toSet() != clusterMarkers.values) {
|
||||
// remove clusters that disappeared
|
||||
clusterMarkers.entries.toList().forEach { (marker, cluster) ->
|
||||
if (!clusters.contains(cluster)) {
|
||||
marker.remove()
|
||||
clusterMarkers.remove(marker)
|
||||
}
|
||||
}
|
||||
|
||||
// add new clusters
|
||||
clusters.forEach { cluster ->
|
||||
if (!clusterMarkers.inverse.contains(cluster)) {
|
||||
val marker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.position(LatLng(cluster.coordinates.lat, cluster.coordinates.lng))
|
||||
.z(clusterZ)
|
||||
.icon(
|
||||
map.bitmapDescriptorFactory.fromBitmap(
|
||||
clusterIconGenerator.makeIcon(
|
||||
cluster.clusterCount.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
.anchor(0.5f, 0.5f)
|
||||
)
|
||||
clusterMarkers[marker] = cluster
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateChargerIcons() {
|
||||
markers.forEach { (m, c) ->
|
||||
m.setIcon(makeIcon(c))
|
||||
m.setAnchor(0.5f, if (mini) 0.5f else 1f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSingleChargerIcon(charger: ChargeLocation) {
|
||||
markers.inverse[charger]?.apply {
|
||||
setIcon(makeIcon(charger))
|
||||
setAnchor(0.5f, if (mini) 0.5f else 1f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeIcon(
|
||||
charger: ChargeLocation,
|
||||
scale: Float = 1f
|
||||
) = chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(charger, filteredConnectors),
|
||||
scale = scale,
|
||||
highlight = charger.id == highlighedCharger?.id,
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti(filteredConnectors),
|
||||
fav = charger.id in favorites,
|
||||
mini = mini
|
||||
)
|
||||
|
||||
private fun animateMarker(charger: ChargeLocation, marker: Marker, appear: Boolean) {
|
||||
val tint = getMarkerTint(charger, filteredConnectors)
|
||||
val highlight = charger.id == highlighedCharger?.id
|
||||
val fault = charger.faultReport != null
|
||||
val multi = charger.isMulti(filteredConnectors)
|
||||
val fav = charger.id in favorites
|
||||
if (appear) {
|
||||
animator.animateMarkerAppear(marker, tint, highlight, fault, multi, fav, mini)
|
||||
} else {
|
||||
animator.animateMarkerDisappear(marker, tint, highlight, fault, multi, fav, mini)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MarkerAnimator(val gen: ChargerIconGenerator) {
|
||||
private val animatingMarkers = hashMapOf<Marker, ValueAnimator>()
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ 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
|
||||
@@ -49,7 +48,6 @@ class ChargepriceViewModel(
|
||||
} else {
|
||||
value = Resource.loading(null)
|
||||
viewModelScope.launch {
|
||||
EspressoIdlingResource.increment()
|
||||
value = try {
|
||||
val result = api.getVehicles()
|
||||
Resource.success(result.filter {
|
||||
@@ -59,8 +57,6 @@ class ChargepriceViewModel(
|
||||
Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
Resource.error(e.message, null)
|
||||
} finally {
|
||||
EspressoIdlingResource.decrement()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,7 +253,6 @@ class ChargepriceViewModel(
|
||||
|
||||
loadPricesJob?.cancel()
|
||||
loadPricesJob = viewModelScope.launch {
|
||||
EspressoIdlingResource.increment()
|
||||
try {
|
||||
val result = api.getChargePrices(
|
||||
ChargepriceRequest(
|
||||
@@ -300,8 +295,6 @@ class ChargepriceViewModel(
|
||||
} catch (e: HttpException) {
|
||||
chargePrices.value = Resource.error(e.message, null)
|
||||
chargePriceMeta.value = Resource.error(e.message, null)
|
||||
} finally {
|
||||
EspressoIdlingResource.decrement()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,16 @@ package net.vonforst.evmap.viewmodel
|
||||
import android.app.Application
|
||||
import android.graphics.Point
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.*
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.switchMap
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.Projection
|
||||
import com.car2go.maps.model.LatLng
|
||||
@@ -24,7 +33,17 @@ import net.vonforst.evmap.api.openchargemap.OCMConnection
|
||||
import net.vonforst.evmap.api.openchargemap.OCMReferenceData
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.model.ChargepointListItem
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import net.vonforst.evmap.model.FILTERS_FAVORITES
|
||||
import net.vonforst.evmap.model.Favorite
|
||||
import net.vonforst.evmap.model.FavoriteWithDetail
|
||||
import net.vonforst.evmap.model.FilterValue
|
||||
import net.vonforst.evmap.model.FilterValues
|
||||
import net.vonforst.evmap.model.getMultipleChoiceValue
|
||||
import net.vonforst.evmap.model.getSliderValue
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
||||
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
|
||||
@@ -266,7 +285,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
|
||||
val favorites: LiveData<List<FavoriteWithDetail>> by lazy {
|
||||
db.favoritesDao().getAllFavorites()
|
||||
db.favoritesDao().getAllFavorites().distinctUntilChanged()
|
||||
}
|
||||
|
||||
val searchResult: MutableLiveData<PlaceWithBounds> by lazy {
|
||||
@@ -282,6 +301,12 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
}
|
||||
|
||||
val mapTrafficSupported: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>().apply {
|
||||
value = false
|
||||
}
|
||||
}
|
||||
|
||||
val mapTrafficEnabled: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>().apply {
|
||||
value = prefs.mapTrafficEnabled
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/referral_tesla"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/ic_tesla" />
|
||||
|
||||
<Button
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/map_details"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
app:goneUnless="@{vm.mapTrafficSupported}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -94,6 +95,7 @@
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/map_traffic"
|
||||
android:checked="@={vm.mapTrafficEnabled}"
|
||||
app:goneUnless="@{vm.mapTrafficSupported}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView23" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
app:startDestination="@id/map"
|
||||
android:id="@+id/nav_graph">
|
||||
|
||||
<navigation
|
||||
|
||||
@@ -199,8 +199,6 @@
|
||||
<string name="autocomplete_connection_error">Nepodařilo se načíst návrhy</string>
|
||||
<string name="pref_language_device_default">Podle zařízení</string>
|
||||
<string name="pref_darkmode_device_default">Podle zařízení</string>
|
||||
<string name="pref_chargeprice_currency_sek">Švédská koruna (SEK)</string>
|
||||
<string name="pref_chargeprice_currency_usd">Americký dolar (USD)</string>
|
||||
<string name="pref_provider_google_maps">Mapy Google</string>
|
||||
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
|
||||
<string name="about_contributors">Přispěvatelé</string>
|
||||
@@ -351,16 +349,6 @@
|
||||
<string name="chargeprice_faq_link">https://ev-map.app/faq/#price-comparison-feature</string>
|
||||
<string name="pref_darkmode_always_on">vždy zapnut</string>
|
||||
<string name="pref_darkmode_always_off">vždy vypnut</string>
|
||||
<string name="pref_chargeprice_currency_chf">Švýcarský frank (CHF)</string>
|
||||
<string name="pref_chargeprice_currency_czk">Česká koruna (CZK)</string>
|
||||
<string name="pref_chargeprice_currency_dkk">Dánská koruna (DKK)</string>
|
||||
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
|
||||
<string name="pref_chargeprice_currency_gbp">Britská libra (GBP)</string>
|
||||
<string name="pref_chargeprice_currency_huf">Maďarský forint (HUF)</string>
|
||||
<string name="pref_chargeprice_currency_hrk">Chorvatská kuna (HRK)</string>
|
||||
<string name="pref_chargeprice_currency_isk">Islandská koruna (ISK)</string>
|
||||
<string name="pref_chargeprice_currency_nok">Norská koruna (NOK)</string>
|
||||
<string name="pref_chargeprice_currency_pln">Polský zlotý (PLN)</string>
|
||||
<string name="chargeprice_header_other_tariffs">Ostatní plány</string>
|
||||
<string name="charger_website">Webové stránky</string>
|
||||
<string name="compass">Kompas</string>
|
||||
@@ -383,4 +371,5 @@
|
||||
<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>
|
||||
<string name="pref_provider_osm">OpenStreetMap</string>
|
||||
</resources>
|
||||
@@ -258,19 +258,8 @@
|
||||
<string name="pref_darkmode_device_default">Geräteeinstellung verwenden</string>
|
||||
<string name="pref_darkmode_always_on">immer an</string>
|
||||
<string name="pref_darkmode_always_off">immer aus</string>
|
||||
<string name="pref_chargeprice_currency_chf">Schweizer Franken (CHF)</string>
|
||||
<string name="pref_chargeprice_currency_czk">Tschechische Krone (CZK)</string>
|
||||
<string name="pref_chargeprice_currency_dkk">Dänische Krone (DKK)</string>
|
||||
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
|
||||
<string name="pref_chargeprice_currency_gbp">Britisches Pfund (GBP)</string>
|
||||
<string name="pref_chargeprice_currency_hrk">Kroatische Kuna (HRK)</string>
|
||||
<string name="pref_chargeprice_currency_huf">Ungarischer Forint (HUF)</string>
|
||||
<string name="pref_chargeprice_currency_isk">Isländische Krone (ISK)</string>
|
||||
<string name="pref_chargeprice_currency_nok">Norwegische Krone (NOK)</string>
|
||||
<string name="pref_chargeprice_currency_pln">Polnischer Złoty (PLN)</string>
|
||||
<string name="pref_chargeprice_currency_sek">Schwedische Krone (SEK)</string>
|
||||
<string name="pref_chargeprice_currency_usd">US-Dollar (USD)</string>
|
||||
<string name="pref_provider_google_maps">Google Maps</string>
|
||||
<string name="pref_provider_osm">OpenStreetMap</string>
|
||||
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
|
||||
<string name="about_contributors">Mitwirkende</string>
|
||||
<string name="about_contributors_text">Dank an alle Mitwirkenden für ihre Beiträge von Code und Übersetzungen für EVMap:</string>
|
||||
@@ -378,4 +367,5 @@
|
||||
<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>
|
||||
<string name="auto_zoom_for_details">Für Details hineinzoomen</string>
|
||||
</resources>
|
||||
@@ -128,9 +128,6 @@
|
||||
<string name="pref_darkmode_device_default">Utiliser le réglage de l\'appareil</string>
|
||||
<string name="pref_darkmode_always_on">toujours allumé</string>
|
||||
<string name="pref_darkmode_always_off">toujours éteint</string>
|
||||
<string name="pref_chargeprice_currency_czk">Couronne tchèque (CZK)</string>
|
||||
<string name="pref_chargeprice_currency_dkk">Couronne danoise (DKK)</string>
|
||||
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
|
||||
<string name="show_more">plus…</string>
|
||||
<string name="fav_remove">Retirer des favoris</string>
|
||||
<string name="amenities">Commodités</string>
|
||||
@@ -230,9 +227,6 @@
|
||||
<item quantity="other">(seront mis en évidence dans la comparaison des prix)</item>
|
||||
</plurals>
|
||||
<string name="deleted_recent_search_results">Les résultats de recherche récents ont été supprimés</string>
|
||||
<string name="pref_chargeprice_currency_gbp">Livre sterling (GBP)</string>
|
||||
<string name="pref_chargeprice_currency_isk">Couronne islandaise (ISK)</string>
|
||||
<string name="pref_chargeprice_currency_nok">Couronne norvégienne (NOK)</string>
|
||||
<string name="settings_charger_data">Stations de recharge</string>
|
||||
<string name="got_it">J\'ai compris</string>
|
||||
<string name="powered_by_mapbox">propulsé par Mapbox</string>
|
||||
@@ -246,15 +240,9 @@
|
||||
<string name="settings_data_sources">Sources de données</string>
|
||||
<string name="data_sources_description">Veuillez choisir une source de données pour les stations de recharge. Vous pourrez la modifier ultérieurement dans les paramètres de l\'application.</string>
|
||||
<string name="pref_search_provider_info">Les données pour la recherche de lieux, en particulier celles de Google Maps, sont relativement coûteuses à récupérer. Veuillez envisager de faire un don via \"À propos\" -> \"Faire un don\".</string>
|
||||
<string name="pref_chargeprice_currency_hrk">Kuna croate (HRK)</string>
|
||||
<string name="help">Aide</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary">Autoriser la charge en courant alternatif monophasé de plus de 4,5 kW</string>
|
||||
<string name="pref_chargeprice_currency_huf">Forint hongrois (HUF)</string>
|
||||
<string name="pref_chargeprice_currency_pln">Złoty polonais (PLN)</string>
|
||||
<string name="pref_map_rotate_gestures_on">Utilisez deux doigts pour faire pivoter la carte</string>
|
||||
<string name="pref_chargeprice_currency_chf">Franc suisse (CHF)</string>
|
||||
<string name="pref_chargeprice_currency_usd">Dollar américain (USD)</string>
|
||||
<string name="pref_chargeprice_currency_sek">Couronne suédoise (SEK)</string>
|
||||
<string name="cost_detail_charging"><b>Recharge %s</b></string>
|
||||
<string name="cost_detail_parking"><b>Stationnement %s</b></string>
|
||||
<string name="navigate">Naviguer vers</string>
|
||||
|
||||
@@ -84,10 +84,6 @@
|
||||
<string name="got_it">Skjønner</string>
|
||||
<string name="settings_data_sources">Datakilder</string>
|
||||
<string name="pref_search_delete_recent">Slett nylige søkeresultater</string>
|
||||
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
|
||||
<string name="pref_chargeprice_currency_nok">Norske kroner (NOK)</string>
|
||||
<string name="pref_chargeprice_currency_gbp">Britiske pund (GBP)</string>
|
||||
<string name="pref_chargeprice_currency_sek">Svenske kroner (SEK)</string>
|
||||
<string name="realtime_data_loading">Sjekker sanntidsstatus …</string>
|
||||
<string name="realtime_data_source">Kilde for sanntidsstatus (beta): %s</string>
|
||||
<string name="realtime_data_unavailable">Sanntidsstatus utilgjengelig</string>
|
||||
@@ -183,16 +179,9 @@
|
||||
<string name="deleted_recent_search_results">Nylige søkeresultater slettet</string>
|
||||
<string name="autocomplete_connection_error">Kunne ikke laste inn forslag</string>
|
||||
<string name="pref_language_device_default">Enhetsforvalg</string>
|
||||
<string name="pref_chargeprice_currency_chf">Sveitserfranc (CHF)</string>
|
||||
<string name="pref_chargeprice_currency_czk">Tsjekkiske kroner (CZK)</string>
|
||||
<string name="pref_chargeprice_currency_dkk">Danske kroner (DKK)</string>
|
||||
<string name="pref_chargeprice_currency_hrk">Kroatiske kroner (HRK)</string>
|
||||
<string name="pref_map_rotate_gestures_on">Bruk to fingre for å rotere kartet</string>
|
||||
<string name="pref_map_rotate_gestures_off">Rotasjon avslått (nord er alltid oppover)</string>
|
||||
<string name="refresh_live_data">oppdater sanntidsstatus</string>
|
||||
<string name="pref_chargeprice_currency_isk">Islandske kroner (ISK)</string>
|
||||
<string name="pref_chargeprice_currency_pln">Polske zloty (PLN)</string>
|
||||
<string name="pref_chargeprice_currency_usd">Amerikanske dollar (USD)</string>
|
||||
<string name="filters_deactivated">Filtre deaktivert</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigasjonsknapp åpner kartprogrammet med laderposisjon</string>
|
||||
<string name="show_more">flere …</string>
|
||||
@@ -217,7 +206,6 @@
|
||||
<string name="filter_exclude_faults">Utelat ladere med rapporterte feil</string>
|
||||
<string name="plug_cee_blau">CEE blå</string>
|
||||
<string name="filter_open_247">Døgnåpent</string>
|
||||
<string name="pref_chargeprice_currency_huf">Ungarske forint (HUF)</string>
|
||||
<string name="donation_dialog_detail">EVMap er fri programvare. Kodebidrag mottas med takk. Overvei å gi din støtte gjennom GitHub-sponsorer for å dekke løpende utgifter.</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary">Tillat enfaselading over 4.5 kW</string>
|
||||
<string name="connectors">Tilkobling</string>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<string name="pref_search_provider_info">Gegevens opzoeken is duur, vooral via Google Maps. Overweeg aub om een donatie te doen via “Over” -> “Doneer”.</string>
|
||||
<string name="data_source_openchargemap_desc">Werelddekkend, met variabele kwaliteit. Beschrijving in Engels of lokale taal. Onderhouden door de gebruikers. Ook open overheidswege eens in sommige landen (bv. Noord-Amerika, UK, Frankrijk, Noorwegen).</string>
|
||||
<string name="pref_darkmode_always_off">altijd uit</string>
|
||||
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
|
||||
<string name="chargeprice_select_car_first">Kiest eerst je voertuig model in de instellingen</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Geen compatibele connectoren aan dit laadstation</string>
|
||||
<string name="license">Licentie</string>
|
||||
@@ -228,17 +227,6 @@
|
||||
<string name="pref_language_device_default">Standaardtaal van toestel</string>
|
||||
<string name="pref_darkmode_device_default">Standaardinstelling van toestel</string>
|
||||
<string name="pref_darkmode_always_on">altijd aan</string>
|
||||
<string name="pref_chargeprice_currency_chf">Zwitserse Frank (CHF)</string>
|
||||
<string name="pref_chargeprice_currency_czk">Tsjechische koruna (CZK)</string>
|
||||
<string name="pref_chargeprice_currency_dkk">Deense kroon (DKK)</string>
|
||||
<string name="pref_chargeprice_currency_gbp">Britse Pond (GBP)</string>
|
||||
<string name="pref_chargeprice_currency_hrk">Kroatische Kuna (HRK)</string>
|
||||
<string name="pref_chargeprice_currency_huf">Hongaarse Forint (HUF)</string>
|
||||
<string name="pref_chargeprice_currency_isk">IJslandse Kroon (ISK)</string>
|
||||
<string name="pref_chargeprice_currency_nok">Noorse Kroon (NOK)</string>
|
||||
<string name="pref_chargeprice_currency_pln">Poolse Złoty (PLN)</string>
|
||||
<string name="pref_chargeprice_currency_sek">Zweedse Kroon (SEK)</string>
|
||||
<string name="pref_chargeprice_currency_usd">Amerikaanse Dollar (USD)</string>
|
||||
<string name="pref_provider_google_maps">Google Maps</string>
|
||||
<string name="edit_filter_profile">“%s” editeren</string>
|
||||
<string name="compass">Kompas</string>
|
||||
|
||||
@@ -177,11 +177,6 @@
|
||||
<string name="pref_language_device_default">Língua do dispositivo</string>
|
||||
<string name="pref_darkmode_device_default">Padrão do dispositivo</string>
|
||||
<string name="pref_darkmode_always_on">Sempre ligado</string>
|
||||
<string name="pref_chargeprice_currency_chf">Franco suíço (CHF)</string>
|
||||
<string name="pref_chargeprice_currency_czk">Coroa checa (CZK)</string>
|
||||
<string name="pref_chargeprice_currency_dkk">Coroa dinamarquesa (DKK)</string>
|
||||
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
|
||||
<string name="pref_chargeprice_currency_gbp">Libra esterlina (GBP)</string>
|
||||
<string name="chargeprice_header_my_tariffs">Os meus planos</string>
|
||||
<string name="chargeprice_header_other_tariffs">Outros planos</string>
|
||||
<string name="developer_options">Opções de desenvolvedor</string>
|
||||
@@ -287,13 +282,6 @@
|
||||
<string name="welcome_1">Encontre carregadores elétricos perto de si</string>
|
||||
<string name="close">Fechar</string>
|
||||
<string name="edit_filter_profile">Editar “%s”</string>
|
||||
<string name="pref_chargeprice_currency_hrk">Cuna croata (HRK)</string>
|
||||
<string name="pref_chargeprice_currency_huf">Florim húngaro (HUF)</string>
|
||||
<string name="pref_chargeprice_currency_isk">Coroa islandesa (ISK)</string>
|
||||
<string name="pref_chargeprice_currency_nok">Coroa norueguesa (NOK)</string>
|
||||
<string name="pref_chargeprice_currency_pln">Złoty polaco (PLN)</string>
|
||||
<string name="pref_chargeprice_currency_sek">Coroa sueca (SEK)</string>
|
||||
<string name="pref_chargeprice_currency_usd">Dólar americano (USD)</string>
|
||||
<string name="pref_provider_google_maps">Google Maps</string>
|
||||
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
|
||||
<string name="about_contributors">Contribuidores</string>
|
||||
@@ -383,4 +371,5 @@
|
||||
<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>
|
||||
<string name="pref_provider_osm">OpenStreetMap</string>
|
||||
</resources>
|
||||
@@ -259,18 +259,6 @@
|
||||
<string name="pref_darkmode_device_default">Implicit dispozitiv</string>
|
||||
<string name="pref_darkmode_always_on">permanent</string>
|
||||
<string name="pref_darkmode_always_off">dezactivat</string>
|
||||
<string name="pref_chargeprice_currency_chf">Franci elvetieni (CHF)</string>
|
||||
<string name="pref_chargeprice_currency_czk">Coroane cehe (CZK)</string>
|
||||
<string name="pref_chargeprice_currency_dkk">Coroane daneze (DKK)</string>
|
||||
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
|
||||
<string name="pref_chargeprice_currency_gbp">Lire sterline (GBP)</string>
|
||||
<string name="pref_chargeprice_currency_hrk">Croatian kuna (HRK)</string>
|
||||
<string name="pref_chargeprice_currency_huf">Forinti maghiari (HUF)</string>
|
||||
<string name="pref_chargeprice_currency_isk">Coroane islandeze (ISK)</string>
|
||||
<string name="pref_chargeprice_currency_nok">Coroane norvegiene (NOK)</string>
|
||||
<string name="pref_chargeprice_currency_pln">Zloti polonezi (PLN)</string>
|
||||
<string name="pref_chargeprice_currency_sek">Coroane suedeze (SEK)</string>
|
||||
<string name="pref_chargeprice_currency_usd">Dolari americani (USD)</string>
|
||||
<string name="pref_provider_google_maps">Google Maps</string>
|
||||
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
|
||||
<string name="about_contributors">Contribuitori</string>
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_language_names">
|
||||
<item>@string/pref_language_device_default</item>
|
||||
<item>@string/pref_language_en</item>
|
||||
<item>@string/pref_language_de</item>
|
||||
<item>@string/pref_language_cs</item>
|
||||
<item>@string/pref_language_fr</item>
|
||||
<item>@string/pref_language_nb_rNO</item>
|
||||
<item>@string/pref_language_nl</item>
|
||||
<item>@string/pref_language_pt</item>
|
||||
<item>@string/pref_language_ro</item>
|
||||
</string-array>
|
||||
<string-array name="pref_language_values" translatable="false">
|
||||
<item>default</item>
|
||||
<item>en</item>
|
||||
<item>de</item>
|
||||
<item>cs</item>
|
||||
<item>fr</item>
|
||||
<item>nb-NO</item>
|
||||
<item>nl</item>
|
||||
<item>pt</item>
|
||||
<item>ro</item>
|
||||
</string-array>
|
||||
<string-array name="pref_darkmode_names">
|
||||
<item>@string/pref_darkmode_device_default</item>
|
||||
<item>@string/pref_darkmode_always_on</item>
|
||||
@@ -32,21 +10,7 @@
|
||||
<item>on</item>
|
||||
<item>off</item>
|
||||
</string-array>
|
||||
<string-array name="pref_chargeprice_currency_names">
|
||||
<item>@string/pref_chargeprice_currency_chf</item>
|
||||
<item>@string/pref_chargeprice_currency_czk</item>
|
||||
<item>@string/pref_chargeprice_currency_dkk</item>
|
||||
<item>@string/pref_chargeprice_currency_eur</item>
|
||||
<item>@string/pref_chargeprice_currency_gbp</item>
|
||||
<item>@string/pref_chargeprice_currency_hrk</item>
|
||||
<item>@string/pref_chargeprice_currency_huf</item>
|
||||
<item>@string/pref_chargeprice_currency_isk</item>
|
||||
<item>@string/pref_chargeprice_currency_nok</item>
|
||||
<item>@string/pref_chargeprice_currency_pln</item>
|
||||
<item>@string/pref_chargeprice_currency_sek</item>
|
||||
<item>@string/pref_chargeprice_currency_usd</item>
|
||||
</string-array>
|
||||
<string-array name="pref_chargeprice_currency_values" translatable="false">
|
||||
<string-array name="pref_chargeprice_currencies" translatable="false">
|
||||
<item>CHF</item>
|
||||
<item>CZK</item>
|
||||
<item>DKK</item>
|
||||
|
||||
@@ -10,14 +10,6 @@
|
||||
<string name="chargeprice_api_url">https://api.chargeprice.app/v1/</string>
|
||||
<string name="fronyx_url">https://fronyx.io/</string>
|
||||
<string name="website_url">https://ev-map.app</string>
|
||||
<string name="pref_language_en">English</string>
|
||||
<string name="pref_language_de">Deutsch</string>
|
||||
<string name="pref_language_fr">Français</string>
|
||||
<string name="pref_language_nb_rNO">Norsk Bokmål</string>
|
||||
<string name="pref_language_nl">Nederlands</string>
|
||||
<string name="pref_language_pt">Português</string>
|
||||
<string name="pref_language_ro">Romana</string>
|
||||
<string name="pref_language_cs">Czech</string>
|
||||
<string name="about_contributors_list">
|
||||
Danilo Bargen\n
|
||||
Altonss\n
|
||||
@@ -48,4 +40,5 @@
|
||||
<string name="referral_eprimo" translatable="false">eprimo</string>
|
||||
<string name="copyright_summary">©2020–2024 Johan von Forstner and contributors</string>
|
||||
<string name="acra_backend_url" translatable="false">https://acra.muc.vonforst.net/report</string>
|
||||
<string name="maplibre_attributionsDialogTitle">MapLibre Maps SDK for Android</string>
|
||||
</resources>
|
||||
|
||||
@@ -258,19 +258,8 @@
|
||||
<string name="pref_darkmode_device_default">Device default</string>
|
||||
<string name="pref_darkmode_always_on">always on</string>
|
||||
<string name="pref_darkmode_always_off">always off</string>
|
||||
<string name="pref_chargeprice_currency_chf">Swiss franc (CHF)</string>
|
||||
<string name="pref_chargeprice_currency_czk">Czech koruna (CZK)</string>
|
||||
<string name="pref_chargeprice_currency_dkk">Danish krone (DKK)</string>
|
||||
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
|
||||
<string name="pref_chargeprice_currency_gbp">Pound sterling (GBP)</string>
|
||||
<string name="pref_chargeprice_currency_hrk">Croatian kuna (HRK)</string>
|
||||
<string name="pref_chargeprice_currency_huf">Hungarian forint (HUF)</string>
|
||||
<string name="pref_chargeprice_currency_isk">Icelandic króna (ISK)</string>
|
||||
<string name="pref_chargeprice_currency_nok">Norwegian krone (NOK)</string>
|
||||
<string name="pref_chargeprice_currency_pln">Polish złoty (PLN)</string>
|
||||
<string name="pref_chargeprice_currency_sek">Swedish krona (SEK)</string>
|
||||
<string name="pref_chargeprice_currency_usd">US dollar (USD)</string>
|
||||
<string name="pref_provider_google_maps">Google Maps</string>
|
||||
<string name="pref_provider_osm">OpenStreetMap</string>
|
||||
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
|
||||
<string name="about_contributors">Contributors</string>
|
||||
<string name="about_contributors_text">Thanks to all contributors for their coding and translation contributions to EVMap:</string>
|
||||
@@ -378,4 +367,5 @@
|
||||
<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>
|
||||
<string name="auto_zoom_for_details">Zoom in to see details</string>
|
||||
</resources>
|
||||
@@ -18,8 +18,7 @@
|
||||
<ListPreference
|
||||
android:key="chargeprice_currency"
|
||||
android:title="@string/pref_chargeprice_currency"
|
||||
android:entries="@array/pref_chargeprice_currency_names"
|
||||
android:entryValues="@array/pref_chargeprice_currency_values"
|
||||
android:entryValues="@array/pref_chargeprice_currencies"
|
||||
android:defaultValue="EUR"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<CheckBoxPreference
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
android:key="language"
|
||||
android:persistent="false"
|
||||
android:title="@string/pref_language"
|
||||
android:entries="@array/pref_language_names"
|
||||
android:entryValues="@array/pref_language_values"
|
||||
android:defaultValue="default"
|
||||
android:summary="%s" />
|
||||
<ListPreference
|
||||
|
||||
6
app/src/normal/java/auto/CarInfo.kt
Normal file
6
app/src/normal/java/auto/CarInfo.kt
Normal file
@@ -0,0 +1,6 @@
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.info.CarInfo
|
||||
|
||||
val CarContext.patchedCarInfo: CarInfo
|
||||
get() = (this.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
|
||||
@@ -7,10 +7,4 @@ fun addDebugInterceptors(context: Context) {
|
||||
|
||||
}
|
||||
|
||||
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder = this
|
||||
|
||||
object EspressoIdlingResource {
|
||||
fun increment() {}
|
||||
|
||||
fun decrement() {}
|
||||
}
|
||||
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder = this
|
||||
@@ -1,16 +1,16 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
val kotlinVersion by extra("1.9.10")
|
||||
val kotlinVersion by extra("1.9.24")
|
||||
val aboutLibsVersion by extra("10.9.1")
|
||||
val navVersion by extra("2.7.5")
|
||||
val navVersion by extra("2.7.7")
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.2.2")
|
||||
classpath("com.android.tools.build:gradle:8.4.2")
|
||||
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")
|
||||
@@ -24,11 +24,7 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
//noinspection JcenterRepositoryObsolete
|
||||
maven { setUrl("https://jitpack.io") }
|
||||
maven {
|
||||
setUrl("https://raw.githubusercontent.com/ev-map/mapbox-gl-native-android/mvn")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,12 @@ be put into the app in the form of a resource file called `apikeys.xml` under
|
||||
<string name="mapbox_key" translatable="false">
|
||||
insert your Mapbox key here
|
||||
</string>
|
||||
<string name="jawg_key" translatable="false">
|
||||
insert your Jawg Maps key here
|
||||
</string>
|
||||
<string name="arcgis_key" translatable="false">
|
||||
insert your ArcGIS Maps key here
|
||||
</string>
|
||||
<string name="goingelectric_key" translatable="false">
|
||||
insert your GoingElectric key here
|
||||
</string>
|
||||
@@ -52,10 +58,12 @@ Map providers
|
||||
|
||||
The different Map SDKs are wrapped by our [fork](https://github.com/ev-map/AnyMaps) of the
|
||||
[AnyMaps](https://github.com/sharenowTech/AnyMaps) library to provide a common API. The `google`
|
||||
build flavor of the app includes both Google Maps and Mapbox and allows the user to switch between
|
||||
the two, while the `foss` flavor only includes the Mapbox SDK.
|
||||
build flavor of the app includes both Google Maps and OpenStreetMap (vector tiles from
|
||||
[Jawg Maps](https://www.jawg.io/en/) through [MapLibre](https://maplibre.org/)) and allows the user
|
||||
to switch between the two, while the `foss` flavor only includes OSM.
|
||||
|
||||
> ⚠️ When testing the app using the Android Emulator, we recommend using Google Maps and not Mapbox, as the latter has
|
||||
> ⚠️ When testing the app using the Android Emulator, we recommend using Google Maps and not
|
||||
> OSM/MapLibre, as the latter has
|
||||
[issues displaying the markers](https://github.com/mapbox/mapbox-gl-native/issues/10829). It works fine on real Android devices.
|
||||
|
||||
### Google Maps
|
||||
@@ -77,9 +85,39 @@ the two, while the `foss` flavor only includes the Mapbox SDK.
|
||||
|
||||
</details>
|
||||
|
||||
### Jawg Maps
|
||||
|
||||
[Dynamic Maps](https://www.jawg.io/docs/apidocs/maps/)
|
||||
|
||||
<details>
|
||||
<summary>How to obtain an API key</summary>
|
||||
|
||||
1. [Sign up](https://www.jawg.io/lab) for a Jawg account
|
||||
2. Under [Access Tokens](https://www.jawg.io/lab/access-tokens), copy your default access token or
|
||||
create a new one. Do not restrict it to a specific origin (this setting is not compatible with
|
||||
Android apps).
|
||||
|
||||
</details>
|
||||
|
||||
### ArcGIS
|
||||
|
||||
[World Imagery basemap](https://www.arcgis.com/home/item.html?id=10df2279f9684e4a9f6a7f08febac2a9)\
|
||||
*We use this for the satellite map, as [Jawg's satellite
|
||||
style](https://blog.jawg.io/satellite-imaging/) does not have global coverage.*
|
||||
|
||||
<details>
|
||||
<summary>How to obtain an API key</summary>
|
||||
|
||||
1. [Sign up](https://developers.arcgis.com/dashboard/) for an ArcGIS developer account
|
||||
2. In the dashboard, copy your default API key or create a new one. It has to have access to the
|
||||
"Basemaps" service.
|
||||
|
||||
</details>
|
||||
|
||||
### Mapbox
|
||||
|
||||
[Maps SDK for Android](https://docs.mapbox.com/android/maps)
|
||||
[Geocoding API](https://docs.mapbox.com/api/search/geocoding/)\
|
||||
*previously we also used Mapbox's Maps SDK, but this has now been switched to Jawg Maps.*
|
||||
|
||||
<details>
|
||||
<summary>How to obtain an API key</summary>
|
||||
@@ -91,7 +129,6 @@ the two, while the `foss` flavor only includes the Mapbox SDK.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
Charging station databases
|
||||
--------------------------
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Funkce:
|
||||
- Zobrazuje všechny nabíjecí stanice z komunitou spravovaných databází GoingElectric.de a Open Charge Map.
|
||||
- Informace o dostupnosti v reálném čase (pouze v Evropě)
|
||||
- Integrované srovnání cen pomocí Chargeprice.app (pouze v Evropě)
|
||||
- Mapové podklady z OpenStreetMap (Mapbox)
|
||||
- Mapové podklady z OpenStreetMap
|
||||
- Vyhledávání míst
|
||||
- Pokročilé možnosti filtrování, včetně uložených profilů filtrů
|
||||
- Seznam oblíbených, také s informacemi o dostupnosti
|
||||
|
||||
7
fastlane/metadata/android/de-DE/changelogs/214.txt
Normal file
7
fastlane/metadata/android/de-DE/changelogs/214.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Änderungen:
|
||||
- OpenStreetMap-Karten: Wechsel der Datenquelle von Mapbox zu Jawg Maps
|
||||
|
||||
Fehler behoben:
|
||||
- App-Shortcuts repariert
|
||||
- Anzeigefehler behoben
|
||||
- Abstürze behoben
|
||||
2
fastlane/metadata/android/de-DE/changelogs/220.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/220.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Unterstützung für Geräte mit OpenGL ES 2.0 wiederhergestellt
|
||||
2
fastlane/metadata/android/de-DE/changelogs/222.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/222.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
3
fastlane/metadata/android/de-DE/changelogs/224.txt
Normal file
3
fastlane/metadata/android/de-DE/changelogs/224.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Fehler behoben:
|
||||
- Probleme mit OpenStreetMap behoben
|
||||
- Abstürze behoben
|
||||
2
fastlane/metadata/android/de-DE/changelogs/226.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/226.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
2
fastlane/metadata/android/de-DE/changelogs/228.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/228.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
2
fastlane/metadata/android/de-DE/changelogs/230.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/230.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
@@ -5,7 +5,7 @@ Funktionen:
|
||||
- Anzeige der Stromtankstellen aus den Stromtankstellenverzeichnissen von GoingElectric.de und Open Charge Map
|
||||
- Echtzeit-Verfügbarkeitsanzeige für viele Ladesäulen (nur in Europa)
|
||||
- Integrierter Preisvergleich für die jeweilige Ladesäule mit Chargeprice.app (nur in Europa)
|
||||
- Kartendaten von OpenStreetMap (Mapbox)
|
||||
- Kartendaten von OpenStreetMap
|
||||
- Suche nach Orten
|
||||
- Erweiterte Filterfunktionen, Filterprofile speichern
|
||||
- Favoritenliste, auch mit Anzeige der Verfügbarkeit
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 14 KiB |
7
fastlane/metadata/android/en-US/changelogs/214.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/214.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Changes:
|
||||
- OpenStreetMap maps: Change data source from Mapbox to Jawg Maps
|
||||
|
||||
Bugfixes:
|
||||
- Fixed app shortcuts
|
||||
- Fixed display errors
|
||||
- Fixed crashes
|
||||
2
fastlane/metadata/android/en-US/changelogs/220.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/220.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- Restored support for devices with OpenGL ES 2.0
|
||||
2
fastlane/metadata/android/en-US/changelogs/222.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/222.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
3
fastlane/metadata/android/en-US/changelogs/224.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/224.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Bugfixes:
|
||||
- Fixed problems with OpenStreetMap
|
||||
- Fixed crashes
|
||||
2
fastlane/metadata/android/en-US/changelogs/226.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/226.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
2
fastlane/metadata/android/en-US/changelogs/228.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/228.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
2
fastlane/metadata/android/en-US/changelogs/230.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/230.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user