Compare commits
5 Commits
yannikblos
...
zy-live-lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1b3fc939a | ||
|
|
d234930464 | ||
|
|
7e75aac135 | ||
|
|
6c3710859b | ||
|
|
d430a2202e |
@@ -1,382 +0,0 @@
|
||||
name: map-generator
|
||||
on:
|
||||
workflow_dispatch: # Manual trigger
|
||||
inputs:
|
||||
jobs:
|
||||
description: 'Which job(s) to run right now?'
|
||||
required: true
|
||||
default: 'all'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- copy-coasts
|
||||
- planet
|
||||
- wiki
|
||||
- isolines
|
||||
- subways
|
||||
- tiger
|
||||
- maps
|
||||
|
||||
env:
|
||||
WIKIMEDIA_USERNAME: ${{ secrets.WIKIMEDIA_USERNAME }}
|
||||
WIKIMEDIA_PASSWORD: ${{ secrets.WIKIMEDIA_PASSWORD }}
|
||||
S3_KEY_ID: ${{ secrets.S3_KEY_ID }}
|
||||
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
|
||||
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
|
||||
S3_BUCKET: ${{ secrets.S3_BUCKET }}
|
||||
SFTP_USER: ${{ secrets.SFTP_USER }}
|
||||
SFTP_PASSWORD: ${{ secrets.SFTP_PASSWORD }}
|
||||
SFTP_HOST: ${{ secrets.SFTP_HOST }}
|
||||
SFTP_PATH: ${{ secrets.SFTP_PATH }}
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
TZ: Etc/UTC
|
||||
|
||||
jobs:
|
||||
copy-coasts:
|
||||
if: inputs.jobs == 'copy-coasts' || inputs.jobs == 'all'
|
||||
name: Copy Previously Generated Coasts
|
||||
runs-on: mapfilemaker
|
||||
container:
|
||||
image: ubuntu:latest
|
||||
volumes:
|
||||
- /media/4tbexternal:/media/4tbexternal
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-map-generator-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Copy Coasts
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f /media/4tbexternal/osm-maps/*/intermediate_data/WorldCoasts.geom ]; then
|
||||
cp /media/4tbexternal/osm-maps/*/intermediate_data/WorldCoasts.geom /media/4tbexternal/osm-planet/latest_coasts.geom
|
||||
cp /media/4tbexternal/osm-maps/*/intermediate_data/WorldCoasts.rawgeom /media/4tbexternal/osm-planet/latest_coasts.rawgeom
|
||||
fi
|
||||
|
||||
update-planet:
|
||||
if: inputs.jobs == 'planet' || inputs.jobs == 'all'
|
||||
name: Update Planet
|
||||
runs-on: mapfilemaker
|
||||
container:
|
||||
image: ubuntu:latest
|
||||
volumes:
|
||||
- /media/4tbexternal:/media/4tbexternal
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-map-generator-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
apt-get update -y
|
||||
apt-get install -y pyosmium osmium-tool python3-venv python3-pip wget2
|
||||
rm -f /usr/lib/python*/EXTERNALLY-MANAGED
|
||||
pip3 install "protobuf<4"
|
||||
- name: Download Planet File if Absent
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -d /media/4tbexternal/osm-planet/planet/ ]; then
|
||||
mkdir -p /media/4tbexternal/osm-planet/planet/
|
||||
fi
|
||||
if [ ! -f /media/4tbexternal/osm-planet/planet/planet-latest.osm.pbf ]; then
|
||||
cd /media/4tbexternal/osm-planet/planet/
|
||||
wget2 --verbose --progress=bar --continue --debug https://ftpmirror.your.org/pub/openstreetmap/pbf/planet-latest.osm.pbf
|
||||
fi
|
||||
- name: Update Planet
|
||||
shell: bash
|
||||
run: |
|
||||
cd /media/4tbexternal/osm-planet/planet/
|
||||
pyosmium-up-to-date planet-latest.osm.pbf -o planet-latest-new.osm.pbf -vv --size 16384
|
||||
mv planet-latest-new.osm.pbf planet-latest.osm.pbf
|
||||
- name: Converting planet-latest.osm.pbf to planet.o5m
|
||||
run: /root/OM/osmctools/osmconvert planet-latest.osm.pbf -o=planet.o5m
|
||||
|
||||
wiki-update:
|
||||
if: inputs.jobs == 'wiki' || inputs.jobs == 'all'
|
||||
name: Update Wikipedia
|
||||
runs-on: mapfilemaker
|
||||
container:
|
||||
image: ubuntu:latest
|
||||
volumes:
|
||||
- /media/4tbexternal:/media/4tbexternal
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-map-generator-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
apt-get update -y
|
||||
apt-get install -y jq curl wget2 rustc cargo git ca-certificates
|
||||
- name: Clone wikiparser if necessary
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -d /media/4tbexternal/wikiparser ]; then
|
||||
cd /media/4tbexternal
|
||||
git clone https://codeberg.org/comaps/wikiparser.git
|
||||
fi
|
||||
- name: Check for planet file
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -f /media/4tbexternal/osm-planet/planet/planet-latest.osm.pbf ]; then
|
||||
echo "ERROR: No file at /media/4tbexternal/osm-planet/planet/planet-latest.osm.pbf"
|
||||
ls -al /media/4tbexternal/
|
||||
ls -al /media/4tbexternal/osm-planet/
|
||||
ls -al /media/4tbexternal/osm-planet/planet/
|
||||
exit 1
|
||||
fi
|
||||
- name: Update Wikipedia from Enterprise API
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p /media/4tbexternal/osm-planet/wikipedia/dumps
|
||||
mkdir -p /media/4tbexternal/osm-planet/wikipedia/build
|
||||
cd /media/4tbexternal/wikiparser
|
||||
ls -al
|
||||
echo "Downloading ..."
|
||||
./download.sh /media/4tbexternal/osm-planet/wikipedia/dumps
|
||||
echo "Running ..."
|
||||
./run.sh /media/4tbexternal/osm-planet/wikipedia/build \
|
||||
/media/4tbexternal/osm-planet/planet/planet-latest.osm.pbf \
|
||||
/media/4tbexternal/osm-planet/wikipedia/dumps/latest/*.tar.gz
|
||||
echo "DONE"
|
||||
|
||||
update-isolines:
|
||||
if: inputs.jobs == 'isolines' || inputs.jobs == 'all'
|
||||
name: Update Isolines
|
||||
runs-on: mapfilemaker
|
||||
container:
|
||||
image: ubuntu:latest
|
||||
volumes:
|
||||
- /media/4tbexternal:/media/4tbexternal
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-map-generator-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
apt-get update -qq \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
osmctools \
|
||||
rclone \
|
||||
git \
|
||||
ca-certificates \
|
||||
openssh-client \
|
||||
sshpass \
|
||||
vim \
|
||||
wget \
|
||||
build-essential \
|
||||
clang \
|
||||
cmake \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3.12-venv \
|
||||
qt6-base-dev \
|
||||
qt6-positioning-dev \
|
||||
libc++-dev \
|
||||
libfreetype-dev \
|
||||
libglvnd-dev \
|
||||
libgl1-mesa-dev \
|
||||
libharfbuzz-dev \
|
||||
libicu-dev \
|
||||
libqt6svg6-dev \
|
||||
libqt6positioning6-plugins \
|
||||
libqt6positioning6 \
|
||||
libsqlite3-dev \
|
||||
libxrandr-dev \
|
||||
libxinerama-dev \
|
||||
libxcursor-dev \
|
||||
libxi-dev \
|
||||
zlib1g-dev
|
||||
rm -f /usr/lib/python*/EXTERNALLY-MANAGED
|
||||
pip3 install "protobuf<4"
|
||||
- name: Clone main repo if necessary
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -d /media/4tbexternal/comaps-init ]; then
|
||||
apt-get update -qq && apt-get install -y --no-install-recommends git
|
||||
cd /media/4tbexternal
|
||||
git clone --recurse-submodules --shallow-submodules -b rebase-generator-pastk-wb251014 --single-branch https://codeberg.org/comaps/comaps.git comaps-init
|
||||
fi
|
||||
- name: Update Isolines
|
||||
shell: bash
|
||||
run: |
|
||||
cd /media/4tbexternal/comaps-init/
|
||||
./tools/unix/build_omim.sh -R topography_generator_tool
|
||||
rm -rf ../osm-planet/isolines/
|
||||
mkdir ../osm-planet/isolines/
|
||||
../omim-build-relwithdebinfo/topography_generator_tool \
|
||||
--profiles_path=./data/conf/isolines/isolines-profiles.json \
|
||||
--countries_to_generate_path=./data/conf/isolines/countries-to-generate.json \
|
||||
--tiles_isolines_out_dir=../osm-planet/isolines/tmp-tiles/ \
|
||||
--countries_isolines_out_dir=../osm-planet/isolines/ \
|
||||
--data_dir=./data/ \
|
||||
--srtm_path=../osm-planet/SRTM-patched-europe/ \
|
||||
--threads=22
|
||||
|
||||
update-subways:
|
||||
if: inputs.jobs == 'subways' || inputs.jobs == 'all'
|
||||
name: Update Subways
|
||||
runs-on: mapfilemaker
|
||||
container:
|
||||
image: ubuntu:latest
|
||||
volumes:
|
||||
- /media/4tbexternal:/media/4tbexternal
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-map-generator-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y --no-install-recommends curl osmctools osmium-tool python3-venv ca-certificates git python3-pip
|
||||
rm -f /usr/lib/python*/EXTERNALLY-MANAGED
|
||||
pip3 install "protobuf<4"
|
||||
- name: Clone subways if necessary
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -d /media/4tbexternal/subways ]; then
|
||||
cd /media/4tbexternal
|
||||
git clone https://codeberg.org/comaps/subways.git
|
||||
fi
|
||||
- name: Clone main repo if necessary
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -d /media/4tbexternal/comaps-init ]; then
|
||||
cd /media/4tbexternal
|
||||
git clone --recurse-submodules --shallow-submodules -b rebase-generator-pastk-wb251014 --single-branch https://codeberg.org/comaps/comaps.git comaps-init
|
||||
fi
|
||||
- name: Update Subways
|
||||
shell: bash
|
||||
run: |
|
||||
cd /media/4tbexternal/comaps-init/
|
||||
cp tools/unix/maps/settings.sh.prod tools/unix/maps/settings.sh
|
||||
./tools/unix/maps/generate_subways.sh
|
||||
|
||||
update-tiger:
|
||||
if: inputs.jobs == 'tiger' || inputs.jobs == 'all'
|
||||
name: Update TIGER
|
||||
runs-on: mapfilemaker
|
||||
container:
|
||||
image: ubuntu:latest
|
||||
volumes:
|
||||
- /media/4tbexternal:/media/4tbexternal
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-map-generator-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
clang \
|
||||
cmake \
|
||||
ninja-build \
|
||||
ca-certificates \
|
||||
git \
|
||||
wget2
|
||||
- name: Clone main repo if necessary
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -d /media/4tbexternal/comaps-init ]; then
|
||||
cd /media/4tbexternal
|
||||
git clone --recurse-submodules --shallow-submodules -b rebase-generator-pastk-wb251014 --single-branch https://codeberg.org/comaps/comaps.git comaps-init
|
||||
fi
|
||||
- name: Build address_parser
|
||||
shell: bash
|
||||
run: |
|
||||
cd /media/4tbexternal/comaps-init
|
||||
rm -rf ../omim-build-relwithdebinfo/CMakeCache.txt
|
||||
rm -rf ../omim-build-relwithdebinfo/CMakeFiles
|
||||
./tools/unix/build_omim.sh -R address_parser_tool
|
||||
- name: Update TIGER from Nominatim
|
||||
shell: bash
|
||||
run: |
|
||||
cd /media/4tbexternal/osm-planet/
|
||||
wget2 https://nominatim.org/data/tiger-nominatim-preprocessed-latest.csv.tar.gz
|
||||
tar -xOzf tiger-nominatim-preprocessed-latest.csv.tar.gz | /media/4tbexternal/omim-build-relwithdebinfo/address_parser_tool --output_path=./tiger
|
||||
|
||||
generate-maps:
|
||||
if: inputs.jobs == 'maps' || inputs.jobs == 'all'
|
||||
name: Generate Maps
|
||||
runs-on: mapfilemaker
|
||||
container:
|
||||
image: ubuntu:latest
|
||||
volumes:
|
||||
- /media/4tbexternal:/media/4tbexternal
|
||||
options: --ulimit nofile=262144:262144
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-map-generator-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
apt-get update -qq \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
osmctools \
|
||||
rclone \
|
||||
git \
|
||||
ca-certificates \
|
||||
openssh-client \
|
||||
sshpass \
|
||||
vim \
|
||||
wget \
|
||||
build-essential \
|
||||
clang \
|
||||
cmake \
|
||||
ninja-build \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3.12-venv \
|
||||
qt6-base-dev \
|
||||
qt6-positioning-dev \
|
||||
libc++-dev \
|
||||
libfreetype-dev \
|
||||
libglvnd-dev \
|
||||
libgl1-mesa-dev \
|
||||
libharfbuzz-dev \
|
||||
libicu-dev \
|
||||
libqt6svg6-dev \
|
||||
libqt6positioning6-plugins \
|
||||
libqt6positioning6 \
|
||||
libsqlite3-dev \
|
||||
libxrandr-dev \
|
||||
libxinerama-dev \
|
||||
libxcursor-dev \
|
||||
libxi-dev \
|
||||
zlib1g-dev
|
||||
- name: Clone repo if necessary
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -d /media/4tbexternal/comaps-init ]; then
|
||||
cd /media/4tbexternal
|
||||
git clone --recurse-submodules --shallow-submodules -b rebase-generator-pastk-wb251014 --single-branch https://codeberg.org/comaps/comaps.git comaps-init
|
||||
fi
|
||||
- name: Make output folders if necessary
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -d /media/4tbexternal/osm-maps ]; then
|
||||
mkdir -p /media/4tbexternal/osm-maps
|
||||
fi
|
||||
- name: Get SRTM if necessary
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -d /media/4tbexternal/osm-planet/SRTM-patched-europe/ ]; then
|
||||
echo "ERROR: NO SRTM"
|
||||
exit 1
|
||||
fi
|
||||
- name: Symlink paths for repo scripts
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p /root/OM
|
||||
ln -s /media/4tbexternal/comaps-init /root/OM/organicmaps
|
||||
ln -s /media/4tbexternal/osm-planet /home/planet
|
||||
ln -s /media/4tbexternal/osm-maps /root/OM/maps_build
|
||||
- name: Run docker_maps_generator.sh
|
||||
shell: bash
|
||||
run: |
|
||||
cd /root/OM/organicmaps
|
||||
./tools/unix/docker_maps_generator.sh
|
||||
@@ -175,10 +175,10 @@ if (NOT PLATFORM_IPHONE AND NOT PLATFORM_ANDROID)
|
||||
find_package(Qt6 COMPONENTS REQUIRED ${qt_components} PATHS $ENV{QT_PATH} /opt/homebrew/opt/qt@6 /usr/local/opt/qt@6 /usr/lib/x86_64-linux-gnu/qt6)
|
||||
|
||||
set(MINIMUM_REQUIRED_QT_VERSION 6.4.0)
|
||||
if (Qt6_VERSION VERSION_LESS ${MINIMUM_REQUIRED_QT_VERSION})
|
||||
message(FATAL_ERROR "Unsupported Qt version: ${Qt6_VERSION}, the minimum required is ${MINIMUM_REQUIRED_QT_VERSION}")
|
||||
if (Qt6Widgets_VERSION VERSION_LESS ${MINIMUM_REQUIRED_QT_VERSION})
|
||||
message(FATAL_ERROR "Unsupported Qt version: ${Qt6Widgets_VERSION}, the minimum required is ${MINIMUM_REQUIRED_QT_VERSION}")
|
||||
else()
|
||||
message(STATUS "Found Qt version: ${Qt6_VERSION}")
|
||||
message(STATUS "Found Qt version: ${Qt6Widgets_VERSION}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
<img src="https://img.shields.io/github/license/comaps/comaps?style=for-the-badge&logo=opensourceinitiative&logoColor=white&color=588157" alt="License"/>
|
||||
</a>
|
||||
<a href="https://github.com/comaps/comaps/actions/workflows/android-check.yaml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/comaps/comaps/.github/workflows/android-check.yaml?label=Android%20Build&logo=android&logoColor=white&style=for-the-badge" alt="Android Build Status"/>
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/comaps/comaps/.github/workflows/android-check.yaml?label=Android%20Build&logo=android&logoColor=white&style=for-the-badge&color=588157" alt="Android Build Status"/>
|
||||
</a>
|
||||
<a href="https://github.com/comaps/comaps/actions/workflows/ios-check.yaml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/comaps/comaps/.github/workflows/ios-check.yaml?label=iOS%20Build&logo=apple&logoColor=white&style=for-the-badge" alt="iOS Build Status"/>
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/comaps/comaps/.github/workflows/ios-check.yaml?label=iOS%20Build&logo=apple&logoColor=white&style=for-the-badge&color=588157" alt="iOS Build Status"/>
|
||||
</a>
|
||||
<a href="https://opencollective.com/comaps">
|
||||
<img src="https://img.shields.io/opencollective/all/comaps?label=Open%20Collective%20Donors&logo=opencollective&logoColor=white&style=for-the-badge&color=588157" alt="Open Collective Donors"/>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
CoMaps - Mapas ensin conexón con privacidá
|
||||
@@ -1 +0,0 @@
|
||||
Лесна навигация - Открийте повече от вашето пътуване - Подкрепен от общността
|
||||
@@ -1 +0,0 @@
|
||||
CoMaps - Хайкинг, Велосипед, Пътуване без Интернет
|
||||
@@ -0,0 +1,9 @@
|
||||
• Vylepšena viditelnost a uživatelské rozhraní pokynů v navigaci
|
||||
• Přidána možnost vynechat kroky
|
||||
• Vylepšeno vyhledávání ve více jazycích
|
||||
• Přidána specifická ikona pro autobusové zastávky
|
||||
• Opraveny problémy s Android Auto (prostřednictvím projektu OM)
|
||||
• Vylepšen editor a opraveny drobné problémy
|
||||
• Vylepšeny styly map (prostřednictvím projektu OM)
|
||||
• Vylepšeny překlady aplikace
|
||||
Další změny najdete v našich poznámkách k vydání Codeberg!
|
||||
@@ -1,8 +1,9 @@
|
||||
• OpenStreetMap-Daten vom 4. November
|
||||
• Aktualisierte Karten-Icons, inkl. Farben für Unterhaltungs-, Sport- & andere Unternehmen
|
||||
• Informationen zu Steckdosen an EV-Ladestationen
|
||||
• Symbole für Sportzentren, Veranstaltungsorte, Massagesalons, Gästehäuser und einige stillgelegte Unternehmen
|
||||
• Verbesserungen bei der Suche
|
||||
• Behebung eines Absturzes bei der Suche
|
||||
• Verbesserte Sprachführung während der Navigation
|
||||
Weitere Änderungen finden in unseren Codeberg-Versionshinweisen!
|
||||
• Verbesserte Sichtbarkeit & Benutzeroberfläche für Navigationsanweisungen
|
||||
• Option um Treppen zu vermeiden
|
||||
• Verbesserte Suche in mehreren Sprachen
|
||||
• Spezifisches Symbol für Busbahnöfe hinzugefügt
|
||||
• Probleme mit Android Auto behoben (via OM)
|
||||
• Verbesserter Editor mit kleinere Bugfixes
|
||||
• Kartenstile verbessert (via OM)
|
||||
• Verbesserte Übersetzungen
|
||||
Für weitere Änderungen siehe Codeberg-Versionshinweise
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
• OpenStreetMap data as of November 4
|
||||
• Recategorized map icons including some new colors for entertainment, sports and other businesses
|
||||
• Display info about available sockets on charging stations
|
||||
• Added bandstands, backless benches and loungers
|
||||
• New icons for different sport centres, event venues, massage salons, guest houses and some disused businesses
|
||||
• Multiple search improvements and crash fix
|
||||
• Improved voice guidance during navigation
|
||||
• Improved visibility and UI of instructions in navigation
|
||||
• Added option to avoid steps
|
||||
• Improved search in multiple languages
|
||||
• Added specific icon for bus stations
|
||||
• Fixed Android Auto issues (via OM project)
|
||||
• Improved editor and fix minor issues
|
||||
• Improved map styles (via OM project)
|
||||
• Improved app translations
|
||||
Check our Codeberg release notes for more changes!
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
• Datos OSM del 04/11
|
||||
• Iconos del mapa recategorizados, incluyendo nuevos colores
|
||||
• Visualización de información sobre enchufes disponibles en estaciones de recarga
|
||||
• Adición de iconos para diferentes centros deportivos, lugares de eventos, salones de masajes, posadas y algunos establecimientos comerciales desactivados
|
||||
• Varias mejoras y correcciones de errores en la búsqueda
|
||||
• Mejora en la orientación por voz durante la navegación
|
||||
• Mejora de la visibilidad y la interfaz de usuario de las instrucciones de navegación
|
||||
• Se ha añadido la opción de evitar escaleras
|
||||
• Mejora de la búsqueda en varios idiomas como ES
|
||||
• Se ha añadido un icono específico para las estaciones de autobús
|
||||
• Se han solucionado los problemas de Android Auto (a través del proyecto OM)
|
||||
• Se ha mejorado el editor y se han solucionado pequeños problemas
|
||||
• Se han mejorado los estilos de los mapas (a través del proyecto OM)
|
||||
Más detalles en Codeberg
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
• Données OpenStreetMap au 4 novembre
|
||||
• Recatégorisation des icônes sur la carte avec ajout de nouvelles couleurs pour certains types de lieux
|
||||
• Affichage des prises sur les bornes électriques
|
||||
• Ajout d'icônes pour les centres sportifs, salles d'événements, salon de massage et autres lieux
|
||||
• Multiple améliorations dans la recherche
|
||||
• Correction d'un plantage dans la recherche
|
||||
• Amélioration de la synthèse vocale durant la navigation
|
||||
• Interface utilisateur et visibilité des instructions en navigation améliorée
|
||||
• Option pour éviter les escaliers ajoutée
|
||||
• Recherche améliorée dans différents languages
|
||||
• Icône pour les gares routières ajoutée
|
||||
• Corrections de bugs liées à Android Auto (via OM)
|
||||
• Editeur amélioré et corrections de bugs
|
||||
• Style de la carte amélioré (via OM)
|
||||
• Traductions améliorées
|
||||
Plus d'informations sur notre Codeberg
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
• Dados OSM de 04/11
|
||||
• Ícones do mapa recategorizados, incluindo novas cores
|
||||
• Exibição de informações sobre tomadas disponíveis em eletropostos
|
||||
• Adição de ícones para diferentes centros esportivos, locais de eventos, salões de massagem, pousadas e alguns estabelecimentos comerciais desativados
|
||||
• Diversas melhorias e correção de erro na busca
|
||||
• Melhoria na orientação por voz durante a navegação
|
||||
Confira nossas notas de lançamento no Codeberg para mais detalhes!
|
||||
• Visibilidade e interface do usuário aprimoradas para instruções na navegação
|
||||
• Opção adicionada para evitar degraus
|
||||
• Busca aprimorada em vários idiomas
|
||||
• Adição de ícone específico para rodoviárias
|
||||
• Problemas corrigidos no Android Auto (via projeto OM)
|
||||
• Editor aprimorado e correção de problemas menores
|
||||
• Estilos de mapa aprimorados (via projeto OM)
|
||||
• Traduções aprimoradas
|
||||
Confira nossas notas de lançamento do Codeberg para mais mudanças!
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
Uma aplicação pela comunidade, grátis e ‘open-source’, de mapas baseada em dados do OpenStreetMap e reforçada com compromisso para transparência, privacidade e sem fins lucrativos. CoMaps é um fork/spin-off de Organic Maps, que, por sua vez, é um fork de Maps.ME
|
||||
|
||||
Leia sobre as razões deste projeto e a sua direção em <b><i>codeberg.org/comaps</i></b>.
|
||||
Junte-se à comunidade e ajude a fazer a melhor aplicação de mapas
|
||||
• Use a aplicação e partilhe-a com outros
|
||||
• Dê ‘feedback’ e reporte problemas
|
||||
• Atualize os dados de mapa na aplicação ou no site do OpenStreetMap
|
||||
|
||||
‣ <b>Simples e Polida</b>: funcionalidades essenciais fáceis que “somente funcionam”.
|
||||
‣ <b>Foco Offline</b>: Planeie e navegue as suas viagens no estrangeiro sem dados móveis, procure locais numa caminhada distante, etc. Todas as funções da aplicação foram criadas com intenção de serem usadas sem internet.
|
||||
‣ <b>Respeita a privacidade</b>: A aplicação foi criada com privacidade em mente — não identifica o utilizador, não rastreia, e não usa a sua informação pessoal. Sem anúncios.
|
||||
‣ <b>Saves Your Battery and Space</b>: Não esgota a sua bateria ao contrário de outras aplicações. Mapas compactos salvam espaço no seu telemóvel.
|
||||
‣ <b>Gratuita e Feita pela Comunidade</b>: Pessoas como si ajudam a criar a aplicação ao adicionar locais ao OpenStreetMap, testando e dando opiniões em funcionalidades e contribuindo com dotes de desenvolvimento e dinheiro.
|
||||
‣ <b>Decisões e Finanças Abertas e Transparentes, Sem fins lucrativos e ‘Open-Source’.</b>
|
||||
|
||||
<b>Funcionalidades principais</b>:
|
||||
• Mapas detalhados descarregáveis com locais que não estão disponíveis com o Google Maps
|
||||
• Modo ao Ar Livre com trilhos de caminhada destacados, acampamentos, fontes de água, cumes, curvas de nível, etc
|
||||
• Caminhos pedestres e ciclovias
|
||||
• Pontos de interesse como restaurantes, estações de serviço, hotéis, lojas, atrações e muitos mais
|
||||
• Pesquise por nome, endereço, ou por categoria de ponto de interesse
|
||||
• Navegação com anúncios de voz ao caminhar, pedalar ou conduzir
|
||||
• Marque os seus locais favoritos com um único clique
|
||||
• Artigos da Wikipédia Offline
|
||||
• Camada de metro e direções
|
||||
• Gravação de Percursos
|
||||
• Exportar e importar marcadores e percursos em formatos KML, KMZ, GPX
|
||||
• Um modo escuro para usar durante a noite
|
||||
• Melhore a informação do mapa para todos com um editor básico embebido
|
||||
|
||||
<b>A liberdade chegou</b>
|
||||
Descubra a sua jornada, navegue o mundo com privacidade e a comunidade à frente!
|
||||
@@ -1,8 +1,9 @@
|
||||
• Карты OpenStreetMap от 4 ноября
|
||||
• Обновлены цвета иконок на карте, добавлены новые цвета для развлечений, спорта, некоторых бизнесов
|
||||
• На зарядных станциях показываются имеющиеся типы разъёмов
|
||||
• Добавлены эстрады, скамейки без спинок и лежаки
|
||||
• Новые иконки для разных спорт центров, массажных салонов, гостевых домов, некоторых закрытых бизнесов
|
||||
• Несколько улучшений и исправлений в поиске
|
||||
• Улучшены голосовые подсказки при навигации
|
||||
Подробнее смотрите на codeberg.org/comaps/comaps/releases
|
||||
• Лучшая видимость и интерфейс при навигации
|
||||
• Добавлена возможность пропускать шаги
|
||||
• Улучшен поиск на нескольких языках
|
||||
• Новый значок автостанций
|
||||
• Исправлены проблемы с Android Auto (через OM)
|
||||
• Улучшен редактор и исправлены мелкие недочёты
|
||||
• Улучшены стили карт (через OM)
|
||||
• Улучшены переводы приложения
|
||||
Ознакомьтесь с примечаниями к выпуску Codeberg, чтобы узнать изменениях!
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
Brezplačno in odprtokodno zemljevidno orodje, ki ga vodi skupnost, temelji na podatkih OpenStreetMap in je okrepljena s predanostjo transparentnosti, zasebnosti in nedobičkonosnosti. CoMaps je izpeljanka OrganicMaps, ta pa je izpeljanka Maps.ME.
|
||||
|
||||
Preverite si o razlogih za ta projekt in njegovi usmerjenosti na <b><i>codeberg.org/comaps</i></b>.
|
||||
Pridružite se skupnosti in pomagajte narediti najboljše zemljevidno orodje
|
||||
• Uporabljajte orodje in širite glas o njem
|
||||
• Dajajte povratne informacije in poročajte o napakah
|
||||
• Posodabljajte podatke zemljevida v tem orodju ali na spletni strani OpenStreetMap
|
||||
|
||||
‣ <b>Osredotočeno na uporabo brez povezave</b>: Načrtujte in se usmerjajte na vašem potovanju v tujini vrez potrebe po mobilnih podatkih, iščite vmesne točke potocanja ko ste na daljšem pohodu ipd. Vse zmogljivosti orodja so zasnovane za delo brez povezave.
|
||||
‣ <b>Spoštovanje zasebnosti</b>: orodje je zasnovano z mislijo na zasebnost – ne prepoznava oseb, ne sledi in ne zbira osebnih podatkov. Brez oglasov.
|
||||
‣ <b>Preprosto in dodelano</b>: nujne zmogljivosti, enostavne za uporabo, ki preprosto delujejo.
|
||||
‣ <b>Prihrani vašo baterijo in prostor.</b>: ne izčrpava vaše baterije kakor druga usmerjevalna orodja. Strnjeni zemljevidi prihranijo dragocen prostor na vašem telefonu.
|
||||
‣ <b>Brezplačno in ustvarjeno v skupnosti</b>: ljudje kot ste vi pomagajo ustvarjati to orodje, tako da dodajajo kraje na OpenStreetMap, preizkušajo in dajejo povratne informacije o zmogljivostih in prispevajo svoje razvijalske sposobnosti in sredstva.
|
||||
‣ <b>Odprto in transparentno odločanje in finance, nedobičkonosno in popolnoma odprtokodno.</b>
|
||||
|
||||
<b>Glavne zmogljivosti</b>:
|
||||
• Prenosljivi podrobni zemljevidi s kraji, ki na Googlovoh zemljevidih niso na voljo.
|
||||
• Prikaz za dejavnosti na prostem s poudarjenimi pohodniškimi potmi, tabornimi prostori, vodnimi viri, vrhovi, plastnicami itd.
|
||||
• Pešpoti in kolesarke poti
|
||||
• Kraji zanimanja, npr. restavracije, bencinske črpalke, hoteli, trgovine, znamenitosti in mnogo več
|
||||
• Iščite po imenu, hišnemu naslovu ali po vrsti
|
||||
• Usmerjanje z glasovnimi obvestili za hojo, kolesarjenje ali vožnjo avtomobila.
|
||||
• Zaznamujte svoje najljubše kraje s preprostim dotikom
|
||||
• Wikipedijini članki brez povezave
|
||||
• Prometna plast podzemne železnice z usmerjanjem
|
||||
• Izvozite ali uvozite zaznamke in sledi v oblikah KML, KMZ, GPX
|
||||
• Temni prikaz za uporabo ponoči
|
||||
• Izboljšajtw podatke zemljevida za vse z uporabo vgrajenega urejevalnika
|
||||
|
||||
<b>Svoboda je tu</b>
|
||||
Odkijte več o vašem potovanju, usmerjajte se po svetu s poudarkom na zasebnosti in skupnostnem delovanju!
|
||||
@@ -1 +1 @@
|
||||
Enostavno usmerjanje – Odkrij več o svojem potovanju – Podprto v skupnosti
|
||||
Enostavna navigacija – Odkrij več o svojem potovanju – Podprto v skupnosti
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Comaps- Vandra, Cykla, Kör Offline, Privat
|
||||
@@ -1,4 +1,4 @@
|
||||
这是一个由社区主导、以 OpenStreetMap 数据为基础的自由开源地图应用,建立在我们对运营透明、隐私安全和非营利性的承诺之上。CoMaps 是 Organic Maps 的分叉/衍生产品,而 Organic Maps 则是 Maps.ME 的分叉。
|
||||
这是一个由社区主导、以 OpenStreetMap 数据为基础的免费开源地图应用,建立在我们对运营透明、隐私安全和非营利性的承诺之上。CoMaps 是 Organic Maps 的分叉/衍生产品,而 Organic Maps 则是 Maps.ME 的分叉。
|
||||
|
||||
如需了解此项目诞生的原因及未来方向,请访问 <b><i>codeberg.org/comaps</i></b>。
|
||||
加入以上社区,和大家一起打造最优质的地图应用
|
||||
@@ -10,7 +10,7 @@
|
||||
‣ <b>尊重隐私</b>:开发者们在设计 CoMaps 时,优先考虑的是保护用户隐私。CoMaps 无法识别用户身份、无法跟踪用户活动,也无法收集个人信息。此外,CoMaps 不会也不能显示任何广告。
|
||||
‣ <b>简洁精致</b>:轻便易用、不出差错的基本功能。
|
||||
‣ <b>节省电池电量和空间</b>:不会像其他导航应用那样耗电。精简的地图可以节省宝贵的手机空间。
|
||||
‣ <b>自由且社区共建</b>:如同您一样的用户通过向 OpenStreetMap 添加地点、测试功能并提供反馈、无私地贡献自己的编程技能和资金,协力开发了 CoMaps。
|
||||
‣ <b>由社区合作创建的免费应用</b>:如同您一样的用户通过向 OpenStreetMap 添加地点、测试功能并提供反馈、无私地贡献自己的编程技能和资金,协力开发了 CoMaps。
|
||||
‣ <b>决策问责、财务透明、非营利性、完全开源。</b>
|
||||
|
||||
<b>主要功能</b>:
|
||||
@@ -25,7 +25,7 @@
|
||||
• 地铁交通图层和路线指示
|
||||
• 轨迹记录
|
||||
• 以 KML、KMZ 和 GPX 格式导出和导入书签和轨迹
|
||||
• 深色模式,适配夜间使用场景
|
||||
• 选择天暗后自动开启的黑暗模式
|
||||
• 使用基本的内置编辑器来编辑 OpenStreetMap 地点,帮助大家改进地图数据
|
||||
|
||||
<b>自由在此</b>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Лесна навигация - Открийте повече от вашето пътуване - Подкрепен от общността
|
||||
@@ -1 +0,0 @@
|
||||
CoMaps - Пътуване с Приватност
|
||||
@@ -1,36 +0,0 @@
|
||||
Uma aplicação pela comunidade, grátis e ‘open-source’, de mapas baseada em dados do OpenStreetMap e reforçada com compromisso para transparência, privacidade e sem fins lucrativos.
|
||||
|
||||
Junte-se à comunidade e ajude a fazer a melhor aplicação de mapas
|
||||
• Use a aplicação e partilhe-a com outros
|
||||
• Dê ‘feedback’ e reporte problemas
|
||||
• Atualize os dados de mapa na aplicação ou no site do OpenStreetMap
|
||||
|
||||
<i>O seu ‘feedback’ e ‘reviews’ de 5 estrelas são a melhor maneira de nos ajudar!</i>
|
||||
|
||||
‣ <b>Simples e Polida</b>: funcionalidades essenciais fáceis que “somente funcionam”.
|
||||
‣ <b>Foco Offline</b>: Planeie e navegue as suas viagens no estrangeiro sem dados móveis, procure locais numa caminhada distante, etc. Todas as funções da aplicação foram criadas com intenção de serem usadas sem internet.
|
||||
‣ <b>Respeita a privacidade</b>: A aplicação foi criada com privacidade em mente — não identifica o utilizador, não rastreia, e não usa a sua informação pessoal. Sem anúncios.
|
||||
‣ <b>Saves Your Battery and Space</b>: Não esgota a sua bateria ao contrário de outras aplicações. Mapas compactos salvam espaço no seu telemóvel.
|
||||
‣ <b>Gratuita e Feita pela Comunidade</b>: Pessoas como si ajudam a criar a aplicação ao adicionar locais ao OpenStreetMap, testando e dando opiniões em funcionalidades e contribuindo com dotes de desenvolvimento e dinheiro.
|
||||
‣ <b>Decisões e Finanças Abertas e Transparentes, Sem fins lucrativos e ‘Open-Source’.</b>
|
||||
|
||||
<b>Funcionalidades principais</b>:
|
||||
• Mapas detalhados descarregáveis com locais que não estão disponíveis com o Google Maps
|
||||
• Modo ao Ar Livre com trilhos de caminhada destacados, acampamentos, fontes de água, cumes, curvas de nível, etc
|
||||
• Caminhos pedestres e ciclovias
|
||||
• Pontos de interesse como restaurantes, estações de serviço, hotéis, lojas, atrações e muitos mais
|
||||
• Pesquise por nome, endereço, ou por categoria de ponto de interesse
|
||||
• Navegação com anúncios de voz ao caminhar, pedalar ou conduzir
|
||||
• Marque os seus locais favoritos com um único clique
|
||||
• Artigos da Wikipédia Offline
|
||||
• Camada de metro e direções
|
||||
• Gravação de Percursos
|
||||
• Exportar e importar marcadores e percursos em formatos KML, KMZ, GPX
|
||||
• Um modo escuro para usar durante a noite
|
||||
• Melhore a informação do mapa para todos com um editor básico embebido
|
||||
• Suporte para ‘Android Auto’
|
||||
|
||||
Por favor, reporte problemas da aplicação, sugira ideias e junte-se à nossa comunidade no website <b><i>comaps.app</i></b>.
|
||||
|
||||
<b>A liberdade chegou</b>
|
||||
Descubra a sua jornada, navegue o mundo com privacidade e a comunidade à frente!
|
||||
@@ -1,36 +0,0 @@
|
||||
Skupnostno vodena brezplačna in odprtokodna aplikacija za zemljevide, ki temelji na podatkih OpenStreetMap, ter je okrepljena z zavezanostjo k transparentnosti, zasebnosti, in ostajanju neprofitne organizacije.
|
||||
|
||||
Pridružite se skupnosti in pomagajte ustvariti najboljšo aplikacijo za zemljevide.
|
||||
• Uporabljajte aplikacijo in jo priporočajte drugim.
|
||||
• Podajte povratne informacije in poročajte o težavah.
|
||||
• Posodobite podatke zemljevida v aplikaciji ali na spletni strani OpenStreetMap.
|
||||
|
||||
<i>Vaše povratne informacije in 5-zvezdične ocene so najboljša podpora za nas!</i>
|
||||
|
||||
‣ <b>Preprostost in izpopolnjenost</b>: bistvene, enostavne za uporabo funkcije, ki preprosto delujejo.
|
||||
‣ <b>Osredotočena na delovanje brez internetne povezave</b>: načrtujte in navigirajte svoje potovanje v tujini brez potrebe po mobilni povezavi, iščite točke na poti med daljšo pohodniško turo, itd. Vse funkcije aplikacije so zasnovane za delovanje brez internetne povezave.
|
||||
‣ <b>Spoštovanje zasebnosti</b>: aplikacija je zasnovana z mislijo na zasebnost – ne identificira ljudi, ne sledi in ne zbira osebnih podatkov. Brez oglasov.
|
||||
‣ <b>Varčuje z baterijo in prostorom</b>: ne izčrpava baterije kot druge navigacijske aplikacije. Kompaktni zemljevidi varčujejo dragoceni prostor na vašem telefonu.
|
||||
‣ <b>Brezplačna in ustvarjena s pomočjo skupnosti</b>: ljudje, kot ste vi, so pomagali ustvariti aplikacijo z dodajanjem krajev v OpenStreetMap, testiranjem in dajanjem povratnih informacij o funkcijah ter prispevanjem svojih razvojnih veščin in denarja.
|
||||
‣ <b>Odprto in pregledno odločanje in finance, neprofitna in popolnoma odprtokodna aplikacija.
|
||||
|
||||
<b>Glavne značilnosti</b>:
|
||||
• Podrobni zemljevidi z mesti, ki niso na voljo v Google Maps, ki jih lahko prenesete
|
||||
• Način za uporabo na prostem z označenimi pohodniškimi potmi, kampi, vodnimi viri, vrhovi, višinskimi krivuljami itd.
|
||||
• Pešpoti in kolesarske poti
|
||||
• Zanimivosti, kot so restavracije, bencinske črpalke, hoteli, trgovine, znamenitosti in še veliko več
|
||||
• Iskanje po imenu, naslovu ali kategoriji zanimivih točk
|
||||
• Navigacija z glasovnimi napovedmi za hojo, kolesarjenje ali vožnjo
|
||||
• Z enim dotikom dodajte svoje priljubljene kraje v zaznamke
|
||||
• Članki iz Wikipedije za uporabo brez internetne povezave
|
||||
• Plast podzemne železnice in navodila za pot
|
||||
• Sledenje poti
|
||||
• Izvoz in uvoz zaznamkov in poti v formatih KML, KMZ, GPX
|
||||
• Temni način za uporabo ponoči
|
||||
• Izboljšajte zemljevidne podatke za vse z uporabo vgrajenega osnovnega urejevalnika.
|
||||
• Podpora za Android Auto.
|
||||
|
||||
Prijavite težave z aplikacijo, predlagajte ideje in se pridružite naši skupnosti na spletni strani <b><i>comaps.app</i></b>.
|
||||
|
||||
<b>Svoboda je tu</b>
|
||||
Odkrijte svojo pot, raziskujte svet z zasebnostjo in skupnostjo v ospredju!
|
||||
@@ -1 +1 @@
|
||||
Enostavno usmerjanje – Odkrij več o svojem potovanju – Podprto v skupnosti
|
||||
Enostavna navigacija – Odkrij več o svojem potovanju – Podprto v skupnosti
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
CoMaps - Usmerjajte zasebno
|
||||
@@ -1 +0,0 @@
|
||||
Comaps- Navigera Privat
|
||||
@@ -1,4 +1,4 @@
|
||||
这是一个由社区主导、以 OpenStreetMap 数据为基础的自由开源地图应用,建立在我们对运营透明、隐私安全和非营利性的承诺之上。
|
||||
这是一个由社区主导、以 OpenStreetMap 数据为基础的免费开源地图应用,建立在我们对运营透明、隐私安全和非营利性的承诺之上。
|
||||
|
||||
加入社区,和大家一起打造最优质的地图应用
|
||||
• 使用 CoMaps 的同时也分享推荐给周围的人
|
||||
@@ -11,7 +11,7 @@
|
||||
‣ <b>以提供离线服务为核心</b>:无需移动网络即可规划和导航您的海外旅行,郊外远足时仍可搜索航点等等。所有功能均可离线使用。
|
||||
‣ <b>尊重隐私</b>:开发者们在设计 CoMaps 时,优先考虑的是保护用户隐私。CoMaps 无法识别用户身份、无法跟踪用户活动,也无法收集个人信息。此外,CoMaps 不会也不能显示任何广告。
|
||||
‣ <b>节省电池电量和空间</b>:不会像其他导航应用那样耗电。精简的地图可以节省宝贵的手机空间。
|
||||
‣ <b>自由且社区共建</b>:如同您一样的用户通过向 OpenStreetMap 添加地点、测试功能并提供反馈、无私地贡献自己的编程技能和资金,协力开发了 CoMaps。
|
||||
‣ <b>由社区合作创建的免费应用</b>:如同您一样的用户通过向 OpenStreetMap 添加地点、测试功能并提供反馈、无私地贡献自己的编程技能和资金,协力开发了 CoMaps。
|
||||
‣ <b>决策问责、财务透明、非营利性、完全开源。</b>
|
||||
|
||||
<b>主要功能</b>:
|
||||
@@ -26,7 +26,7 @@
|
||||
• 地铁交通图层和路线指示
|
||||
• 轨迹记录
|
||||
• 以 KML、KMZ 和 GPX 格式导出和导入书签和轨迹
|
||||
• 深色模式,适配夜间使用场景
|
||||
• 选择天暗后自动开启的黑暗模式
|
||||
• 使用基本的内置编辑器来编辑 OpenStreetMap 地点,帮助大家改进地图数据
|
||||
• 支持 Android Auto
|
||||
|
||||
|
||||
@@ -500,6 +500,13 @@
|
||||
android:stopWithTask="false"
|
||||
/>
|
||||
|
||||
<service android:name=".location.LocationSharingService"
|
||||
android:foregroundServiceType="location"
|
||||
android:exported="false"
|
||||
android:enabled="true"
|
||||
android:stopWithTask="false"
|
||||
/>
|
||||
|
||||
<service
|
||||
android:name=".downloader.DownloaderService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
|
||||
@@ -426,19 +426,32 @@ public class MwmActivity extends BaseMwmFragmentActivity
|
||||
private void shareMyLocation()
|
||||
{
|
||||
final Location loc = MwmApplication.from(this).getLocationHelper().getSavedLocation();
|
||||
if (loc != null)
|
||||
if (loc == null)
|
||||
{
|
||||
SharingUtils.shareLocation(this, loc);
|
||||
dismissLocationErrorDialog();
|
||||
mLocationErrorDialog = new MaterialAlertDialogBuilder(MwmActivity.this, R.style.MwmTheme_AlertDialog)
|
||||
.setMessage(R.string.unknown_current_position)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setOnDismissListener(dialog -> mLocationErrorDialog = null)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
dismissLocationErrorDialog();
|
||||
mLocationErrorDialog = new MaterialAlertDialogBuilder(MwmActivity.this, R.style.MwmTheme_AlertDialog)
|
||||
.setMessage(R.string.unknown_current_position)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setOnDismissListener(dialog -> mLocationErrorDialog = null)
|
||||
.show();
|
||||
SharingUtils.shareLocation(this, loc);
|
||||
}
|
||||
|
||||
public void onLocationSharingStateChanged(boolean isSharing)
|
||||
{
|
||||
mMapButtonsViewModel.setLocationSharingState(isSharing);
|
||||
MapButtonsController mapButtonsController =
|
||||
(MapButtonsController) getSupportFragmentManager().findFragmentById(R.id.map_buttons);
|
||||
if (mapButtonsController != null)
|
||||
mapButtonsController.updateMenuBadge();
|
||||
|
||||
// Update share location button color in navigation menu
|
||||
if (mNavigationController != null)
|
||||
mNavigationController.refreshShareLocationColor();
|
||||
}
|
||||
|
||||
private void showDownloader(boolean openDownloaded)
|
||||
@@ -1683,6 +1696,13 @@ public class MwmActivity extends BaseMwmFragmentActivity
|
||||
mMapButtonsViewModel.setLayoutMode(MapButtonsController.LayoutMode.regular);
|
||||
refreshLightStatusBar();
|
||||
Utils.keepScreenOn(Config.isKeepScreenOnEnabled(), getWindow());
|
||||
|
||||
// Stop location sharing when navigation ends
|
||||
if (app.organicmaps.location.LocationSharingManager.getInstance().isSharing())
|
||||
{
|
||||
app.organicmaps.location.LocationSharingManager.getInstance().stopSharing();
|
||||
onLocationSharingStateChanged(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package app.organicmaps.api;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.organicmaps.sdk.util.log.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* HTTP API client for location sharing server.
|
||||
* Sends encrypted location updates to the server.
|
||||
*/
|
||||
public class LocationSharingApiClient
|
||||
{
|
||||
private static final String TAG = LocationSharingApiClient.class.getSimpleName();
|
||||
|
||||
private static final int CONNECT_TIMEOUT_MS = 10000;
|
||||
private static final int READ_TIMEOUT_MS = 10000;
|
||||
|
||||
private final String mServerBaseUrl;
|
||||
private final String mSessionId;
|
||||
private final Executor mExecutor;
|
||||
|
||||
public interface Callback
|
||||
{
|
||||
void onSuccess();
|
||||
void onFailure(@NonNull String error);
|
||||
}
|
||||
|
||||
public LocationSharingApiClient(@NonNull String serverBaseUrl, @NonNull String sessionId)
|
||||
{
|
||||
mServerBaseUrl = serverBaseUrl.endsWith("/") ? serverBaseUrl : serverBaseUrl + "/";
|
||||
mSessionId = sessionId;
|
||||
mExecutor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session on the server.
|
||||
* @param callback Result callback
|
||||
*/
|
||||
public void createSession(@Nullable Callback callback)
|
||||
{
|
||||
mExecutor.execute(() -> {
|
||||
try
|
||||
{
|
||||
String url = mServerBaseUrl + "api/v1/session";
|
||||
String requestBody = "{\"sessionId\":\"" + mSessionId + "\"}";
|
||||
|
||||
int responseCode = postJson(url, requestBody);
|
||||
|
||||
if (responseCode >= 200 && responseCode < 300)
|
||||
{
|
||||
Logger.d(TAG, "Session created successfully: " + mSessionId);
|
||||
if (callback != null)
|
||||
callback.onSuccess();
|
||||
}
|
||||
else
|
||||
{
|
||||
String error = "Server returned error: " + responseCode;
|
||||
Logger.w(TAG, error);
|
||||
if (callback != null)
|
||||
callback.onFailure(error);
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Logger.e(TAG, "Failed to create session", e);
|
||||
if (callback != null)
|
||||
callback.onFailure(e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update location on the server with encrypted payload.
|
||||
* @param encryptedPayloadJson Encrypted payload JSON (from native code)
|
||||
* @param callback Result callback
|
||||
*/
|
||||
public void updateLocation(@NonNull String encryptedPayloadJson, @Nullable Callback callback)
|
||||
{
|
||||
mExecutor.execute(() -> {
|
||||
try
|
||||
{
|
||||
String url = mServerBaseUrl + "api/v1/location/" + mSessionId;
|
||||
|
||||
int responseCode = postJson(url, encryptedPayloadJson);
|
||||
|
||||
if (responseCode >= 200 && responseCode < 300)
|
||||
{
|
||||
Logger.d(TAG, "Location updated successfully");
|
||||
if (callback != null)
|
||||
callback.onSuccess();
|
||||
}
|
||||
else
|
||||
{
|
||||
String error = "Server returned error: " + responseCode;
|
||||
Logger.w(TAG, error);
|
||||
if (callback != null)
|
||||
callback.onFailure(error);
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Logger.e(TAG, "Failed to update location", e);
|
||||
if (callback != null)
|
||||
callback.onFailure(e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* End the session on the server.
|
||||
*/
|
||||
public void endSession()
|
||||
{
|
||||
mExecutor.execute(() -> {
|
||||
try
|
||||
{
|
||||
String url = mServerBaseUrl + "api/v1/session/" + mSessionId;
|
||||
deleteRequest(url);
|
||||
Logger.d(TAG, "Session ended: " + mSessionId);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Logger.e(TAG, "Failed to end session", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a POST request with JSON body.
|
||||
* @param urlString URL to send request to
|
||||
* @param jsonBody JSON request body
|
||||
* @return HTTP response code
|
||||
* @throws IOException on network error
|
||||
*/
|
||||
private int postJson(@NonNull String urlString, @NonNull String jsonBody) throws IOException
|
||||
{
|
||||
URL url = new URL(urlString);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
|
||||
try
|
||||
{
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
connection.setReadTimeout(READ_TIMEOUT_MS);
|
||||
connection.setDoOutput(true);
|
||||
|
||||
// Write body
|
||||
byte[] bodyBytes = jsonBody.getBytes(StandardCharsets.UTF_8);
|
||||
connection.setFixedLengthStreamingMode(bodyBytes.length);
|
||||
|
||||
try (OutputStream os = connection.getOutputStream())
|
||||
{
|
||||
os.write(bodyBytes);
|
||||
os.flush();
|
||||
}
|
||||
|
||||
return connection.getResponseCode();
|
||||
}
|
||||
finally
|
||||
{
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a DELETE request.
|
||||
* @param urlString URL to send request to
|
||||
* @return HTTP response code
|
||||
* @throws IOException on network error
|
||||
*/
|
||||
private int deleteRequest(@NonNull String urlString) throws IOException
|
||||
{
|
||||
URL url = new URL(urlString);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
|
||||
try
|
||||
{
|
||||
connection.setRequestMethod("DELETE");
|
||||
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
connection.setReadTimeout(READ_TIMEOUT_MS);
|
||||
|
||||
return connection.getResponseCode();
|
||||
}
|
||||
finally
|
||||
{
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
package app.organicmaps.background;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.OutOfQuotaPolicy;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
@@ -37,11 +35,7 @@ public class OsmUploadWork extends Worker
|
||||
if (Editor.nativeHasSomethingToUpload() && OsmOAuth.isAuthorized())
|
||||
{
|
||||
final Constraints c = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();
|
||||
OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(OsmUploadWork.class).setConstraints(c);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST);
|
||||
}
|
||||
final OneTimeWorkRequest wr = builder.build();
|
||||
final OneTimeWorkRequest wr = new OneTimeWorkRequest.Builder(OsmUploadWork.class).setConstraints(c).build();
|
||||
WorkManager.getInstance(context).beginUniqueWork("UploadOsmChanges", ExistingWorkPolicy.KEEP, wr).enqueue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,15 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import app.organicmaps.R;
|
||||
|
||||
public class BaseMwmDialogFragment extends DialogFragment
|
||||
{
|
||||
@StyleRes
|
||||
protected final int getFullscreenTheme()
|
||||
{
|
||||
return R.style.MwmTheme_DialogFragment_Fullscreen;
|
||||
}
|
||||
|
||||
protected int getStyle()
|
||||
{
|
||||
|
||||
@@ -188,7 +188,7 @@ public class BookmarkCategoriesFragment extends BaseMwmRecyclerFragment<Bookmark
|
||||
ArrayList<MenuBottomSheetItem> items = new ArrayList<>();
|
||||
if (mSelectedCategory != null)
|
||||
{
|
||||
items.add(new MenuBottomSheetItem(R.string.edit, R.drawable.ic_edit,
|
||||
items.add(new MenuBottomSheetItem(R.string.edit, R.drawable.ic_settings,
|
||||
() -> onSettingsActionSelected(mSelectedCategory)));
|
||||
items.add(new MenuBottomSheetItem(mSelectedCategory.isVisible() ? R.string.hide : R.string.show,
|
||||
mSelectedCategory.isVisible() ? R.drawable.ic_hide : R.drawable.ic_show,
|
||||
|
||||
@@ -466,10 +466,12 @@ public class BookmarkListAdapter extends RecyclerView.Adapter<Holders.BaseBookma
|
||||
View desc = inflater.inflate(R.layout.item_category_description, parent, false);
|
||||
MaterialTextView moreBtn = desc.findViewById(R.id.more_btn);
|
||||
MaterialTextView text = desc.findViewById(R.id.text);
|
||||
MaterialTextView title = desc.findViewById(R.id.title);
|
||||
setMoreButtonVisibility(text, moreBtn);
|
||||
holder = new Holders.DescriptionViewHolder(desc, mSectionsDataSource.getCategory());
|
||||
text.setOnClickListener(v -> onMoreButtonClicked(text, moreBtn));
|
||||
moreBtn.setOnClickListener(v -> onMoreButtonClicked(text, moreBtn));
|
||||
title.setOnClickListener(v -> onMoreButtonClicked(text, moreBtn));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -282,11 +282,11 @@ public class BookmarksListFragment extends BaseMwmRecyclerFragment<ConcatAdapter
|
||||
{
|
||||
if (isEmptySearchResults())
|
||||
{
|
||||
requirePlaceholder().setContent(R.string.search_not_found, R.string.search_not_found_query, R.drawable.ic_search_fail);
|
||||
requirePlaceholder().setContent(R.string.search_not_found, R.string.search_not_found_query);
|
||||
}
|
||||
else if (isEmpty())
|
||||
{
|
||||
requirePlaceholder().setContent(R.string.bookmarks_empty_list_title, R.string.bookmarks_empty_list_message, R.drawable.ic_bookmarks);
|
||||
requirePlaceholder().setContent(R.string.bookmarks_empty_list_title, R.string.bookmarks_empty_list_message);
|
||||
}
|
||||
|
||||
boolean isEmptyRecycler = isEmpty() || isEmptySearchResults();
|
||||
@@ -771,7 +771,7 @@ public class BookmarksListFragment extends BaseMwmRecyclerFragment<ConcatAdapter
|
||||
items.add(new MenuBottomSheetItem(R.string.export_file_gpx, R.drawable.ic_file_gpx,
|
||||
() -> onShareOptionSelected(KmlFileType.Gpx)));
|
||||
}
|
||||
items.add(new MenuBottomSheetItem(R.string.edit, R.drawable.ic_edit, this::onSettingsOptionSelected));
|
||||
items.add(new MenuBottomSheetItem(R.string.edit, R.drawable.ic_settings, this::onSettingsOptionSelected));
|
||||
if (!isLastOwnedCategory())
|
||||
items.add(new MenuBottomSheetItem(R.string.delete_list, R.drawable.ic_delete, this::onDeleteOptionSelected));
|
||||
return items;
|
||||
|
||||
@@ -438,17 +438,21 @@ public class Holders
|
||||
static final float SPACING_MULTIPLE = 1.0f;
|
||||
static final float SPACING_ADD = 0.0f;
|
||||
@NonNull
|
||||
private final MaterialTextView mTitle;
|
||||
@NonNull
|
||||
private final MaterialTextView mDescText;
|
||||
|
||||
DescriptionViewHolder(@NonNull View itemView, @NonNull BookmarkCategory category)
|
||||
{
|
||||
super(itemView);
|
||||
mDescText = itemView.findViewById(R.id.text);
|
||||
mTitle = itemView.findViewById(R.id.title);
|
||||
}
|
||||
|
||||
@Override
|
||||
void bind(@NonNull SectionPosition position, @NonNull BookmarkListAdapter.SectionsDataSource sectionsDataSource)
|
||||
{
|
||||
mTitle.setText(sectionsDataSource.getCategory().getName());
|
||||
bindDescription(sectionsDataSource.getCategory());
|
||||
}
|
||||
|
||||
@@ -458,12 +462,9 @@ public class Holders
|
||||
|
||||
String formattedDesc = desc.replace("\n", "<br>");
|
||||
Spanned spannedDesc = Utils.fromHtml(formattedDesc);
|
||||
if (!TextUtils.isEmpty(spannedDesc)) {
|
||||
mDescText.setText(spannedDesc);
|
||||
}
|
||||
else {
|
||||
mDescText.setText(R.string.list_description_empty);
|
||||
}
|
||||
mDescText.setText(spannedDesc);
|
||||
|
||||
UiUtils.showIf(!TextUtils.isEmpty(spannedDesc), mDescText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,7 @@ public class DrivingOptionsScreen extends BaseMapScreen
|
||||
new DrivingOption(RoadType.Dirty, R.string.avoid_unpaved),
|
||||
new DrivingOption(RoadType.Ferry, R.string.avoid_ferry),
|
||||
new DrivingOption(RoadType.Motorway, R.string.avoid_motorways),
|
||||
new DrivingOption(RoadType.Steps, R.string.avoid_steps),
|
||||
new DrivingOption(RoadType.Paved, R.string.avoid_paved)};
|
||||
new DrivingOption(RoadType.Steps, R.string.avoid_steps)};
|
||||
|
||||
@NonNull
|
||||
private final Map<RoadType, Boolean> mInitialDrivingOptionsState = new HashMap<>();
|
||||
|
||||
@@ -222,10 +222,10 @@ public class DownloaderFragment
|
||||
return;
|
||||
|
||||
if (mAdapter != null && mAdapter.isSearchResultsMode())
|
||||
placeholder.setContent(R.string.search_not_found, R.string.search_not_found_query, R.drawable.ic_search_fail);
|
||||
placeholder.setContent(R.string.search_not_found, R.string.search_not_found_query);
|
||||
else
|
||||
placeholder.setContent(R.string.downloader_no_downloaded_maps_title,
|
||||
R.string.downloader_no_downloaded_maps_message, R.drawable.ic_download);
|
||||
R.string.downloader_no_downloaded_maps_message);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -352,8 +352,7 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe
|
||||
{
|
||||
hasChargeSockets = hasChargeSockets || (type == Metadata.MetadataType.FMD_CHARGE_SOCKETS.toInt());
|
||||
}
|
||||
// Hide socket until https://codeberg.org/comaps/comaps/issues/2368 is fixed
|
||||
//UiUtils.showIf(hasChargeSockets, mCardChargingStation);
|
||||
UiUtils.showIf(hasChargeSockets, mCardChargingStation);
|
||||
|
||||
setCardVisibility(mCardDetails, mDetailsBlocks, editableDetails);
|
||||
setCardVisibility(mCardSocialMedia, mSocialMediaBlocks, editableDetails);
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
package app.organicmaps.editor;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.timepicker.MaterialTimePicker;
|
||||
import com.google.android.material.timepicker.TimeFormat;
|
||||
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.sdk.editor.data.HoursMinutes;
|
||||
import app.organicmaps.sdk.util.DateUtils;
|
||||
|
||||
public class FromToTimePicker
|
||||
{
|
||||
private final FragmentActivity mActivity;
|
||||
private final FragmentManager mFragmentManager;
|
||||
private final OnPickListener mListener;
|
||||
private final int mId;
|
||||
private final boolean mIs24HourFormat;
|
||||
private final Resources mResources;
|
||||
|
||||
private HoursMinutes mFromTime;
|
||||
private HoursMinutes mToTime;
|
||||
private MaterialTimePicker mToTimePicker;
|
||||
private MaterialTimePicker mFromTimePicker;
|
||||
private boolean mIsFromTimePicked;
|
||||
private int mInputMode;
|
||||
|
||||
public static void pickTime(@NonNull Fragment fragment,
|
||||
@NonNull FromToTimePicker.OnPickListener listener,
|
||||
@NonNull HoursMinutes fromTime,
|
||||
@NonNull HoursMinutes toTime,
|
||||
int id,
|
||||
boolean startWithToTime)
|
||||
{
|
||||
FromToTimePicker timePicker = new FromToTimePicker(fragment,
|
||||
listener,
|
||||
fromTime,
|
||||
toTime,
|
||||
id);
|
||||
|
||||
if (startWithToTime)
|
||||
timePicker.showToTimePicker();
|
||||
else
|
||||
timePicker.showFromTimePicker();
|
||||
}
|
||||
|
||||
private FromToTimePicker(@NonNull Fragment fragment,
|
||||
@NonNull FromToTimePicker.OnPickListener listener,
|
||||
@NonNull HoursMinutes fromTime,
|
||||
@NonNull HoursMinutes toTime,
|
||||
int id)
|
||||
{
|
||||
mActivity = fragment.requireActivity();
|
||||
mFragmentManager = fragment.getChildFragmentManager();
|
||||
mListener = listener;
|
||||
mFromTime = fromTime;
|
||||
mToTime = toTime;
|
||||
mId = id;
|
||||
|
||||
mIsFromTimePicked = false;
|
||||
mInputMode = MaterialTimePicker.INPUT_MODE_CLOCK;
|
||||
|
||||
mIs24HourFormat = DateUtils.is24HourFormat(mActivity);
|
||||
mResources = mActivity.getResources();
|
||||
|
||||
mActivity.addOnConfigurationChangedListener(this::handleConfigurationChanged);
|
||||
}
|
||||
|
||||
public void showFromTimePicker()
|
||||
{
|
||||
if (mFromTimePicker != null)
|
||||
{
|
||||
saveState(mFromTimePicker, true);
|
||||
mFromTimePicker.dismiss();
|
||||
}
|
||||
|
||||
mFromTimePicker = buildFromTimePicker();
|
||||
mFromTimePicker.show(mFragmentManager, null);
|
||||
}
|
||||
|
||||
public void showToTimePicker()
|
||||
{
|
||||
if (mToTimePicker != null)
|
||||
{
|
||||
saveState(mToTimePicker, false);
|
||||
mToTimePicker.dismiss();
|
||||
}
|
||||
|
||||
mToTimePicker = buildToTimePicker();
|
||||
|
||||
mToTimePicker.show(mFragmentManager, null);
|
||||
}
|
||||
|
||||
private MaterialTimePicker buildFromTimePicker()
|
||||
{
|
||||
MaterialTimePicker timePicker = buildTimePicker(mFromTime,
|
||||
mResources.getString(R.string.editor_time_from),
|
||||
mResources.getString(R.string.next_button),
|
||||
null);
|
||||
|
||||
timePicker.addOnNegativeButtonClickListener(view -> finishTimePicking(false));
|
||||
|
||||
timePicker.addOnPositiveButtonClickListener(view ->
|
||||
{
|
||||
mIsFromTimePicked = true;
|
||||
saveState(timePicker, true);
|
||||
mFromTimePicker = null;
|
||||
showToTimePicker();
|
||||
});
|
||||
|
||||
timePicker.addOnCancelListener(view -> finishTimePicking(false));
|
||||
|
||||
return timePicker;
|
||||
}
|
||||
|
||||
private MaterialTimePicker buildToTimePicker()
|
||||
{
|
||||
MaterialTimePicker timePicker = buildTimePicker(mToTime,
|
||||
mResources.getString(R.string.editor_time_to),
|
||||
null,
|
||||
mResources.getString(R.string.back));
|
||||
|
||||
timePicker.addOnNegativeButtonClickListener(view ->
|
||||
{
|
||||
saveState(timePicker, false);
|
||||
mToTimePicker = null;
|
||||
if (mIsFromTimePicked)
|
||||
showFromTimePicker();
|
||||
else
|
||||
finishTimePicking(false);
|
||||
});
|
||||
|
||||
timePicker.addOnPositiveButtonClickListener(view ->
|
||||
{
|
||||
saveState(timePicker, false);
|
||||
finishTimePicking(true);
|
||||
});
|
||||
|
||||
timePicker.addOnCancelListener(view -> finishTimePicking(false));
|
||||
|
||||
return timePicker;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private MaterialTimePicker buildTimePicker(@NonNull HoursMinutes time,
|
||||
@NonNull String title,
|
||||
@Nullable String positiveButtonTextOverride,
|
||||
@Nullable String negativeButtonTextOverride)
|
||||
{
|
||||
MaterialTimePicker.Builder builder = new MaterialTimePicker.Builder()
|
||||
.setTitleText(title)
|
||||
.setTimeFormat(mIs24HourFormat ? TimeFormat.CLOCK_24H : TimeFormat.CLOCK_12H)
|
||||
.setInputMode(mInputMode)
|
||||
.setTheme(R.style.MwmMain_MaterialTimePicker)
|
||||
.setHour((int) time.hours)
|
||||
.setMinute((int) time.minutes);
|
||||
|
||||
if (positiveButtonTextOverride != null)
|
||||
builder.setPositiveButtonText(positiveButtonTextOverride);
|
||||
|
||||
if (negativeButtonTextOverride != null)
|
||||
builder.setNegativeButtonText(negativeButtonTextOverride);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void saveState(@NonNull MaterialTimePicker timePicker, boolean isFromTime)
|
||||
{
|
||||
mInputMode = timePicker.getInputMode();
|
||||
if (isFromTime)
|
||||
mFromTime = getHoursMinutes(timePicker);
|
||||
else
|
||||
mToTime = getHoursMinutes(timePicker);
|
||||
}
|
||||
|
||||
private HoursMinutes getHoursMinutes(@NonNull MaterialTimePicker timePicker)
|
||||
{
|
||||
return new HoursMinutes(timePicker.getHour(), timePicker.getMinute(), mIs24HourFormat);
|
||||
}
|
||||
|
||||
private void finishTimePicking(boolean isConfirmed)
|
||||
{
|
||||
mActivity.removeOnConfigurationChangedListener(this::handleConfigurationChanged);
|
||||
|
||||
if (isConfirmed)
|
||||
mListener.onHoursMinutesPicked(mFromTime, mToTime, mId);
|
||||
}
|
||||
|
||||
private void handleConfigurationChanged(Configuration configuration)
|
||||
{
|
||||
if (mFromTimePicker != null && mFromTimePicker.isVisible())
|
||||
showFromTimePicker();
|
||||
else if (mToTimePicker != null && mToTimePicker.isVisible())
|
||||
showToTimePicker();
|
||||
}
|
||||
|
||||
public interface OnPickListener
|
||||
{
|
||||
void onHoursMinutesPicked(HoursMinutes from, HoursMinutes to, int id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package app.organicmaps.editor;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.os.Bundle;
|
||||
import android.text.format.DateFormat;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TimePicker;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.base.BaseMwmDialogFragment;
|
||||
import app.organicmaps.sdk.editor.data.HoursMinutes;
|
||||
import app.organicmaps.sdk.util.DateUtils;
|
||||
import app.organicmaps.util.Utils;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.textview.MaterialTextView;
|
||||
|
||||
public class HoursMinutesPickerFragment extends BaseMwmDialogFragment
|
||||
{
|
||||
private static final String EXTRA_FROM = "HoursMinutesFrom";
|
||||
private static final String EXTRA_TO = "HoursMinutesTo";
|
||||
private static final String EXTRA_SELECT_FIRST = "SelectedTab";
|
||||
private static final String EXTRA_ID = "Id";
|
||||
|
||||
public static final int TAB_FROM = 0;
|
||||
public static final int TAB_TO = 1;
|
||||
|
||||
private HoursMinutes mFrom;
|
||||
private HoursMinutes mTo;
|
||||
|
||||
private TimePicker mPicker;
|
||||
private View mPickerHoursLabel;
|
||||
|
||||
@IntRange(from = 0, to = 1)
|
||||
private int mSelectedTab;
|
||||
private TabLayout mTabs;
|
||||
|
||||
private int mId;
|
||||
private Button mOkButton;
|
||||
|
||||
public interface OnPickListener
|
||||
{
|
||||
void onHoursMinutesPicked(HoursMinutes from, HoursMinutes to, int id);
|
||||
}
|
||||
|
||||
public static void pick(Context context, FragmentManager manager, @NonNull HoursMinutes from,
|
||||
@NonNull HoursMinutes to, @IntRange(from = 0, to = 1) int selectedPosition, int id)
|
||||
{
|
||||
final Bundle args = new Bundle();
|
||||
args.putParcelable(EXTRA_FROM, from);
|
||||
args.putParcelable(EXTRA_TO, to);
|
||||
args.putInt(EXTRA_SELECT_FIRST, selectedPosition);
|
||||
args.putInt(EXTRA_ID, id);
|
||||
|
||||
final HoursMinutesPickerFragment fragment = (HoursMinutesPickerFragment) manager.getFragmentFactory().instantiate(
|
||||
context.getClassLoader(), HoursMinutesPickerFragment.class.getName());
|
||||
fragment.setArguments(args);
|
||||
fragment.show(manager, null);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState)
|
||||
{
|
||||
readArgs();
|
||||
final View root = createView();
|
||||
// noinspection ConstantConditions
|
||||
mTabs.getTabAt(mSelectedTab).select();
|
||||
|
||||
final AlertDialog dialog =
|
||||
new MaterialAlertDialogBuilder(requireActivity(), R.style.MwmMain_DialogFragment_TimePicker)
|
||||
.setView(root)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setCancelable(true)
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(dialogInterface -> {
|
||||
mOkButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
mOkButton.setOnClickListener(v -> {
|
||||
if (mSelectedTab == TAB_FROM)
|
||||
{
|
||||
// noinspection ConstantConditions
|
||||
mTabs.getTabAt(TAB_TO).select();
|
||||
return;
|
||||
}
|
||||
|
||||
saveHoursMinutes();
|
||||
dismiss();
|
||||
if (getParentFragment() instanceof OnPickListener)
|
||||
((OnPickListener) getParentFragment()).onHoursMinutesPicked(mFrom, mTo, mId);
|
||||
});
|
||||
refreshPicker();
|
||||
});
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private void readArgs()
|
||||
{
|
||||
final Bundle args = getArguments();
|
||||
if (args == null)
|
||||
throw new IllegalArgumentException("Args must not be null");
|
||||
mFrom = Utils.getParcelable(args, EXTRA_FROM, HoursMinutes.class);
|
||||
mTo = Utils.getParcelable(args, EXTRA_TO, HoursMinutes.class);
|
||||
mSelectedTab = args.getInt(EXTRA_SELECT_FIRST);
|
||||
mId = args.getInt(EXTRA_ID);
|
||||
}
|
||||
|
||||
private View createView()
|
||||
{
|
||||
final LayoutInflater inflater = LayoutInflater.from(requireActivity());
|
||||
@SuppressLint("InflateParams")
|
||||
final View root = inflater.inflate(R.layout.fragment_timetable_picker, null);
|
||||
|
||||
mPicker = root.findViewById(R.id.picker);
|
||||
mPicker.setIs24HourView(DateFormat.is24HourFormat(requireActivity()));
|
||||
@SuppressLint("DiscouragedApi")
|
||||
int id = getResources().getIdentifier("hours", "id", "android");
|
||||
if (id != 0)
|
||||
{
|
||||
mPickerHoursLabel = mPicker.findViewById(id);
|
||||
if (!(mPickerHoursLabel instanceof TextView))
|
||||
mPickerHoursLabel = null;
|
||||
}
|
||||
|
||||
mTabs = root.findViewById(R.id.tabs);
|
||||
MaterialTextView tabView = (MaterialTextView) inflater.inflate(R.layout.tab_timepicker, mTabs, false);
|
||||
tabView.setText(getResources().getString(R.string.editor_time_from));
|
||||
final ColorStateList textColor =
|
||||
AppCompatResources.getColorStateList(requireContext(), R.color.accent_color_selector);
|
||||
tabView.setTextColor(textColor);
|
||||
mTabs.addTab(mTabs.newTab().setCustomView(tabView), true);
|
||||
tabView = (MaterialTextView) inflater.inflate(R.layout.tab_timepicker, mTabs, false);
|
||||
tabView.setText(getResources().getString(R.string.editor_time_to));
|
||||
tabView.setTextColor(textColor);
|
||||
mTabs.addTab(mTabs.newTab().setCustomView(tabView), true);
|
||||
mTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab)
|
||||
{
|
||||
if (!isInit())
|
||||
return;
|
||||
|
||||
saveHoursMinutes();
|
||||
mSelectedTab = tab.getPosition();
|
||||
refreshPicker();
|
||||
if (mPickerHoursLabel != null)
|
||||
mPickerHoursLabel.performClick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab)
|
||||
{}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab)
|
||||
{}
|
||||
});
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private void saveHoursMinutes()
|
||||
{
|
||||
boolean is24HourFormat = DateUtils.is24HourFormat(requireContext());
|
||||
final HoursMinutes hoursMinutes =
|
||||
new HoursMinutes(mPicker.getCurrentHour(), mPicker.getCurrentMinute(), is24HourFormat);
|
||||
if (mSelectedTab == TAB_FROM)
|
||||
mFrom = hoursMinutes;
|
||||
else
|
||||
mTo = hoursMinutes;
|
||||
}
|
||||
|
||||
private boolean isInit()
|
||||
{
|
||||
return mOkButton != null && mPicker != null;
|
||||
}
|
||||
|
||||
private void refreshPicker()
|
||||
{
|
||||
if (!isInit())
|
||||
return;
|
||||
|
||||
HoursMinutes hoursMinutes;
|
||||
int okBtnRes;
|
||||
if (mSelectedTab == TAB_FROM)
|
||||
{
|
||||
hoursMinutes = mFrom;
|
||||
okBtnRes = R.string.next_button;
|
||||
}
|
||||
else
|
||||
{
|
||||
hoursMinutes = mTo;
|
||||
okBtnRes = R.string.ok;
|
||||
}
|
||||
mPicker.setCurrentMinute((int) hoursMinutes.minutes);
|
||||
mPicker.setCurrentHour((int) hoursMinutes.hours);
|
||||
mOkButton.setText(okBtnRes);
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,9 @@ public class PhoneListAdapter extends RecyclerView.Adapter<PhoneListAdapter.View
|
||||
|
||||
deleteButton = itemView.findViewById(R.id.delete_icon);
|
||||
deleteButton.setOnClickListener(this);
|
||||
// TODO: setting icons from code because icons defined in layout XML are white.
|
||||
deleteButton.setImageResource(R.drawable.ic_delete);
|
||||
((ShapeableImageView) itemView.findViewById(R.id.phone_icon)).setImageResource(R.drawable.ic_phone);
|
||||
}
|
||||
|
||||
public void setPosition(int position)
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -30,13 +29,13 @@ import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
class SimpleTimetableAdapter extends RecyclerView.Adapter<SimpleTimetableAdapter.BaseTimetableViewHolder>
|
||||
implements FromToTimePicker.OnPickListener, TimetableProvider
|
||||
implements HoursMinutesPickerFragment.OnPickListener, TimetableProvider
|
||||
{
|
||||
private static final int TYPE_TIMETABLE = 0;
|
||||
private static final int TYPE_ADD_TIMETABLE = 1;
|
||||
|
||||
private static final int ID_OPENING_TIME = 0;
|
||||
private static final int ID_CLOSED_SPAN = 1;
|
||||
private static final int ID_OPENING = 0;
|
||||
private static final int ID_CLOSING = 1;
|
||||
|
||||
private static final int[] DAYS = {R.id.day1, R.id.day2, R.id.day3, R.id.day4, R.id.day5, R.id.day6, R.id.day7};
|
||||
|
||||
@@ -70,7 +69,7 @@ class SimpleTimetableAdapter extends RecyclerView.Adapter<SimpleTimetableAdapter
|
||||
@Override
|
||||
public String getTimetables()
|
||||
{
|
||||
return OpeningHours.nativeTimetablesToString(mItems.toArray(new Timetable[0]));
|
||||
return OpeningHours.nativeTimetablesToString(mItems.toArray(new Timetable[mItems.size()]));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -102,7 +101,7 @@ class SimpleTimetableAdapter extends RecyclerView.Adapter<SimpleTimetableAdapter
|
||||
|
||||
private void addTimetable()
|
||||
{
|
||||
mItems.add(OpeningHours.nativeGetComplementTimetable(mItems.toArray(new Timetable[0])));
|
||||
mItems.add(OpeningHours.nativeGetComplementTimetable(mItems.toArray(new Timetable[mItems.size()])));
|
||||
notifyItemInserted(mItems.size() - 1);
|
||||
refreshComplement();
|
||||
}
|
||||
@@ -116,31 +115,25 @@ class SimpleTimetableAdapter extends RecyclerView.Adapter<SimpleTimetableAdapter
|
||||
|
||||
private void refreshComplement()
|
||||
{
|
||||
mComplementItem = OpeningHours.nativeGetComplementTimetable(mItems.toArray(new Timetable[0]));
|
||||
mComplementItem = OpeningHours.nativeGetComplementTimetable(mItems.toArray(new Timetable[mItems.size()]));
|
||||
notifyItemChanged(getItemCount() - 1);
|
||||
}
|
||||
|
||||
private void pickTime(int position,
|
||||
@IntRange(from = ID_OPENING_TIME, to = ID_CLOSED_SPAN) int id,
|
||||
boolean startWithToTime)
|
||||
@IntRange(from = HoursMinutesPickerFragment.TAB_FROM, to = HoursMinutesPickerFragment.TAB_TO)
|
||||
int tab, @IntRange(from = ID_OPENING, to = ID_CLOSING) int id)
|
||||
{
|
||||
final Timetable data = mItems.get(position);
|
||||
mPickingPosition = position;
|
||||
|
||||
FromToTimePicker.pickTime(mFragment,
|
||||
this,
|
||||
data.workingTimespan.start,
|
||||
data.workingTimespan.end,
|
||||
id,
|
||||
startWithToTime);
|
||||
|
||||
HoursMinutesPickerFragment.pick(mFragment.requireActivity(), mFragment.getChildFragmentManager(),
|
||||
data.workingTimespan.start, data.workingTimespan.end, tab, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHoursMinutesPicked(HoursMinutes from, HoursMinutes to, int id)
|
||||
{
|
||||
final Timetable item = mItems.get(mPickingPosition);
|
||||
if (id == ID_OPENING_TIME)
|
||||
if (id == ID_OPENING)
|
||||
mItems.set(mPickingPosition, OpeningHours.nativeSetOpeningTime(item, new Timespan(from, to)));
|
||||
else
|
||||
mItems.set(mPickingPosition, OpeningHours.nativeAddClosedSpan(item, new Timespan(from, to)));
|
||||
@@ -155,7 +148,7 @@ class SimpleTimetableAdapter extends RecyclerView.Adapter<SimpleTimetableAdapter
|
||||
|
||||
private void addWorkingDay(int day, int position)
|
||||
{
|
||||
final Timetable[] tts = mItems.toArray(new Timetable[0]);
|
||||
final Timetable[] tts = mItems.toArray(new Timetable[mItems.size()]);
|
||||
mItems = new ArrayList<>(Arrays.asList(OpeningHours.nativeAddWorkingDay(tts, position, day)));
|
||||
refreshComplement();
|
||||
notifyDataSetChanged();
|
||||
@@ -163,7 +156,7 @@ class SimpleTimetableAdapter extends RecyclerView.Adapter<SimpleTimetableAdapter
|
||||
|
||||
private void removeWorkingDay(int day, int position)
|
||||
{
|
||||
final Timetable[] tts = mItems.toArray(new Timetable[0]);
|
||||
final Timetable[] tts = mItems.toArray(new Timetable[mItems.size()]);
|
||||
mItems = new ArrayList<>(Arrays.asList(OpeningHours.nativeRemoveWorkingDay(tts, position, day)));
|
||||
refreshComplement();
|
||||
notifyDataSetChanged();
|
||||
@@ -269,13 +262,13 @@ class SimpleTimetableAdapter extends RecyclerView.Adapter<SimpleTimetableAdapter
|
||||
{
|
||||
final int id = v.getId();
|
||||
if (id == R.id.time_open)
|
||||
pickTime(getBindingAdapterPosition(), ID_OPENING_TIME, false);
|
||||
pickTime(getBindingAdapterPosition(), HoursMinutesPickerFragment.TAB_FROM, ID_OPENING);
|
||||
else if (id == R.id.time_close)
|
||||
pickTime(getBindingAdapterPosition(), ID_OPENING_TIME, true);
|
||||
pickTime(getBindingAdapterPosition(), HoursMinutesPickerFragment.TAB_TO, ID_OPENING);
|
||||
else if (id == R.id.tv__remove_timetable)
|
||||
removeTimetable(getBindingAdapterPosition());
|
||||
else if (id == R.id.tv__add_closed)
|
||||
pickTime(getBindingAdapterPosition(), ID_CLOSED_SPAN, false);
|
||||
pickTime(getBindingAdapterPosition(), HoursMinutesPickerFragment.TAB_FROM, ID_CLOSING);
|
||||
else if (id == R.id.allday)
|
||||
swAllday.toggle();
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.base.BaseMwmRecyclerFragment;
|
||||
import app.organicmaps.sdk.editor.data.HoursMinutes;
|
||||
|
||||
public class SimpleTimetableFragment extends BaseMwmRecyclerFragment<SimpleTimetableAdapter>
|
||||
implements TimetableProvider
|
||||
implements TimetableProvider, HoursMinutesPickerFragment.OnPickListener
|
||||
{
|
||||
private SimpleTimetableAdapter mAdapter;
|
||||
@Nullable
|
||||
@@ -56,4 +57,10 @@ public class SimpleTimetableFragment extends BaseMwmRecyclerFragment<SimpleTimet
|
||||
{
|
||||
mInitTimetables = timetables;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHoursMinutesPicked(HoursMinutes from, HoursMinutes to, int id)
|
||||
{
|
||||
mAdapter.onHoursMinutesPicked(from, to, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package app.organicmaps.location;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* AES-256-GCM encryption/decryption for location data.
|
||||
*/
|
||||
public class LocationCrypto
|
||||
{
|
||||
private static final String ALGORITHM = "AES/GCM/NoPadding";
|
||||
private static final int GCM_IV_LENGTH = 12; // 96 bits
|
||||
private static final int GCM_TAG_LENGTH = 128; // 128 bits
|
||||
|
||||
/**
|
||||
* Encrypt plaintext JSON using AES-256-GCM.
|
||||
* @param base64Key Base64-encoded 256-bit key
|
||||
* @param plaintextJson JSON string to encrypt
|
||||
* @return JSON string with encrypted payload: {"iv":"...","ciphertext":"...","authTag":"..."}
|
||||
*/
|
||||
@Nullable
|
||||
public static String encrypt(@NonNull String base64Key, @NonNull String plaintextJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Decode the base64 key
|
||||
byte[] key = Base64.decode(base64Key, Base64.NO_WRAP);
|
||||
if (key.length != 32) // 256 bits
|
||||
{
|
||||
android.util.Log.e("LocationCrypto", "Invalid key size: " + key.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate random IV
|
||||
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||
new SecureRandom().nextBytes(iv);
|
||||
|
||||
// Create cipher
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
|
||||
|
||||
// Encrypt
|
||||
byte[] plaintext = plaintextJson.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] ciphertextWithTag = cipher.doFinal(plaintext);
|
||||
|
||||
// Split ciphertext and auth tag
|
||||
// In GCM mode, doFinal() returns ciphertext + tag
|
||||
int ciphertextLength = ciphertextWithTag.length - (GCM_TAG_LENGTH / 8);
|
||||
byte[] ciphertext = new byte[ciphertextLength];
|
||||
byte[] authTag = new byte[GCM_TAG_LENGTH / 8];
|
||||
|
||||
System.arraycopy(ciphertextWithTag, 0, ciphertext, 0, ciphertextLength);
|
||||
System.arraycopy(ciphertextWithTag, ciphertextLength, authTag, 0, authTag.length);
|
||||
|
||||
// Build JSON response
|
||||
JSONObject result = new JSONObject();
|
||||
result.put("iv", Base64.encodeToString(iv, Base64.NO_WRAP));
|
||||
result.put("ciphertext", Base64.encodeToString(ciphertext, Base64.NO_WRAP));
|
||||
result.put("authTag", Base64.encodeToString(authTag, Base64.NO_WRAP));
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
android.util.Log.e("LocationCrypto", "Encryption failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt encrypted payload using AES-256-GCM.
|
||||
* @param base64Key Base64-encoded 256-bit key
|
||||
* @param encryptedPayloadJson JSON string with format: {"iv":"...","ciphertext":"...","authTag":"..."}
|
||||
* @return Decrypted plaintext JSON string
|
||||
*/
|
||||
@Nullable
|
||||
public static String decrypt(@NonNull String base64Key, @NonNull String encryptedPayloadJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse encrypted payload
|
||||
JSONObject payload = new JSONObject(encryptedPayloadJson);
|
||||
byte[] iv = Base64.decode(payload.getString("iv"), Base64.NO_WRAP);
|
||||
byte[] ciphertext = Base64.decode(payload.getString("ciphertext"), Base64.NO_WRAP);
|
||||
byte[] authTag = Base64.decode(payload.getString("authTag"), Base64.NO_WRAP);
|
||||
|
||||
// Decode the base64 key
|
||||
byte[] key = Base64.decode(base64Key, Base64.NO_WRAP);
|
||||
if (key.length != 32) // 256 bits
|
||||
{
|
||||
android.util.Log.e("LocationCrypto", "Invalid key size: " + key.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Combine ciphertext and auth tag for GCM decryption
|
||||
byte[] ciphertextWithTag = new byte[ciphertext.length + authTag.length];
|
||||
System.arraycopy(ciphertext, 0, ciphertextWithTag, 0, ciphertext.length);
|
||||
System.arraycopy(authTag, 0, ciphertextWithTag, ciphertext.length, authTag.length);
|
||||
|
||||
// Create cipher
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
|
||||
|
||||
// Decrypt
|
||||
byte[] plaintext = cipher.doFinal(ciphertextWithTag);
|
||||
|
||||
return new String(plaintext, StandardCharsets.UTF_8);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
android.util.Log.e("LocationCrypto", "Decryption failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package app.organicmaps.location;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.util.SharingUtils;
|
||||
|
||||
/**
|
||||
* Dialog for starting/stopping live location sharing and managing the share URL.
|
||||
*/
|
||||
public class LocationSharingDialog extends DialogFragment
|
||||
{
|
||||
private static final String TAG = LocationSharingDialog.class.getSimpleName();
|
||||
|
||||
@Nullable
|
||||
private TextView mStatusText;
|
||||
@Nullable
|
||||
private TextView mShareUrlText;
|
||||
@Nullable
|
||||
private Button mStartStopButton;
|
||||
@Nullable
|
||||
private Button mCopyButton;
|
||||
@Nullable
|
||||
private Button mShareButton;
|
||||
|
||||
private LocationSharingManager mManager;
|
||||
|
||||
public static void show(@NonNull FragmentManager fragmentManager)
|
||||
{
|
||||
LocationSharingDialog dialog = new LocationSharingDialog();
|
||||
dialog.show(fragmentManager, TAG);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
mManager = LocationSharingManager.getInstance();
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_location_sharing, null);
|
||||
|
||||
initViews(view);
|
||||
updateUI();
|
||||
|
||||
builder.setView(view);
|
||||
builder.setTitle(R.string.location_sharing_title);
|
||||
builder.setNegativeButton(R.string.close, (dialog, which) -> dismiss());
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
private void initViews(@NonNull View root)
|
||||
{
|
||||
mStatusText = root.findViewById(R.id.status_text);
|
||||
mShareUrlText = root.findViewById(R.id.share_url_text);
|
||||
mStartStopButton = root.findViewById(R.id.start_stop_button);
|
||||
mCopyButton = root.findViewById(R.id.copy_button);
|
||||
mShareButton = root.findViewById(R.id.share_button);
|
||||
|
||||
if (mStartStopButton != null)
|
||||
{
|
||||
mStartStopButton.setOnClickListener(v -> {
|
||||
if (mManager.isSharing())
|
||||
stopSharing();
|
||||
else
|
||||
startSharing();
|
||||
});
|
||||
}
|
||||
|
||||
if (mCopyButton != null)
|
||||
{
|
||||
mCopyButton.setOnClickListener(v -> copyUrl());
|
||||
}
|
||||
|
||||
if (mShareButton != null)
|
||||
{
|
||||
mShareButton.setOnClickListener(v -> shareUrl());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUI()
|
||||
{
|
||||
boolean isSharing = mManager.isSharing();
|
||||
|
||||
if (mStatusText != null)
|
||||
{
|
||||
mStatusText.setText(isSharing
|
||||
? R.string.location_sharing_status_active
|
||||
: R.string.location_sharing_status_inactive);
|
||||
}
|
||||
|
||||
if (mShareUrlText != null)
|
||||
{
|
||||
String url = mManager.getShareUrl();
|
||||
if (url != null && isSharing)
|
||||
{
|
||||
mShareUrlText.setText(url);
|
||||
mShareUrlText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else
|
||||
{
|
||||
mShareUrlText.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (mStartStopButton != null)
|
||||
{
|
||||
mStartStopButton.setText(isSharing
|
||||
? R.string.location_sharing_stop
|
||||
: R.string.location_sharing_start);
|
||||
}
|
||||
|
||||
// Show/hide copy and share buttons
|
||||
int visibility = isSharing ? View.VISIBLE : View.GONE;
|
||||
if (mCopyButton != null)
|
||||
mCopyButton.setVisibility(visibility);
|
||||
if (mShareButton != null)
|
||||
mShareButton.setVisibility(visibility);
|
||||
}
|
||||
|
||||
private void startSharing()
|
||||
{
|
||||
String shareUrl = mManager.startSharing();
|
||||
|
||||
if (shareUrl != null)
|
||||
{
|
||||
Toast.makeText(requireContext(),
|
||||
R.string.location_sharing_started,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
updateUI();
|
||||
|
||||
// Notify the activity
|
||||
if (getActivity() instanceof app.organicmaps.MwmActivity)
|
||||
{
|
||||
((app.organicmaps.MwmActivity) getActivity()).onLocationSharingStateChanged(true);
|
||||
}
|
||||
|
||||
// Auto-copy URL to clipboard
|
||||
copyUrlToClipboard(shareUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
Toast.makeText(requireContext(),
|
||||
R.string.location_sharing_failed_to_start,
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopSharing()
|
||||
{
|
||||
mManager.stopSharing();
|
||||
|
||||
Toast.makeText(requireContext(),
|
||||
R.string.location_sharing_stopped,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
updateUI();
|
||||
|
||||
// Notify the activity
|
||||
if (getActivity() instanceof app.organicmaps.MwmActivity)
|
||||
{
|
||||
((app.organicmaps.MwmActivity) getActivity()).onLocationSharingStateChanged(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyUrl()
|
||||
{
|
||||
String url = mManager.getShareUrl();
|
||||
if (url != null)
|
||||
{
|
||||
copyUrlToClipboard(url);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyUrlToClipboard(@NonNull String url)
|
||||
{
|
||||
ClipboardManager clipboard = (ClipboardManager)
|
||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
if (clipboard != null)
|
||||
{
|
||||
ClipData clip = ClipData.newPlainText("Location Share URL", url);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
|
||||
Toast.makeText(requireContext(),
|
||||
R.string.location_sharing_url_copied,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void shareUrl()
|
||||
{
|
||||
String url = mManager.getShareUrl();
|
||||
if (url == null)
|
||||
return;
|
||||
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType("text/plain");
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.location_sharing_share_message, url));
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.location_sharing_share_url)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package app.organicmaps.location;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.BatteryManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.organicmaps.MwmApplication;
|
||||
import app.organicmaps.sdk.routing.RoutingController;
|
||||
import app.organicmaps.sdk.util.Config;
|
||||
import app.organicmaps.sdk.util.log.Logger;
|
||||
|
||||
/**
|
||||
* Singleton manager for live location sharing functionality.
|
||||
* Coordinates between LocationHelper, RoutingController, and LocationSharingService.
|
||||
*/
|
||||
public class LocationSharingManager
|
||||
{
|
||||
private static final String TAG = LocationSharingManager.class.getSimpleName();
|
||||
|
||||
private static LocationSharingManager sInstance;
|
||||
|
||||
@Nullable
|
||||
private String mSessionId;
|
||||
@Nullable
|
||||
private String mEncryptionKey;
|
||||
@Nullable
|
||||
private String mShareUrl;
|
||||
private boolean mIsSharing = false;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private LocationSharingManager()
|
||||
{
|
||||
mContext = MwmApplication.sInstance;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static synchronized LocationSharingManager getInstance()
|
||||
{
|
||||
if (sInstance == null)
|
||||
sInstance = new LocationSharingManager();
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start live location sharing.
|
||||
* @return Share URL that can be sent to others
|
||||
*/
|
||||
@Nullable
|
||||
public String startSharing()
|
||||
{
|
||||
if (mIsSharing)
|
||||
{
|
||||
Logger.w(TAG, "Location sharing already active");
|
||||
return mShareUrl;
|
||||
}
|
||||
|
||||
// Generate session credentials via native code
|
||||
String[] credentials = nativeGenerateSessionCredentials();
|
||||
if (credentials == null || credentials.length != 2)
|
||||
{
|
||||
Logger.e(TAG, "Failed to generate session credentials");
|
||||
return null;
|
||||
}
|
||||
|
||||
mSessionId = credentials[0];
|
||||
mEncryptionKey = credentials[1];
|
||||
|
||||
// Generate share URL using configured server
|
||||
String serverUrl = Config.LocationSharing.getServerUrl();
|
||||
mShareUrl = nativeGenerateShareUrl(mSessionId, mEncryptionKey, serverUrl);
|
||||
if (mShareUrl == null)
|
||||
{
|
||||
Logger.e(TAG, "Failed to generate share URL");
|
||||
return null;
|
||||
}
|
||||
|
||||
mIsSharing = true;
|
||||
|
||||
// Start foreground service
|
||||
Intent intent = new Intent(mContext, LocationSharingService.class);
|
||||
intent.putExtra(LocationSharingService.EXTRA_SESSION_ID, mSessionId);
|
||||
intent.putExtra(LocationSharingService.EXTRA_ENCRYPTION_KEY, mEncryptionKey);
|
||||
intent.putExtra(LocationSharingService.EXTRA_SERVER_URL, serverUrl);
|
||||
intent.putExtra(LocationSharingService.EXTRA_UPDATE_INTERVAL, Config.LocationSharing.getUpdateInterval());
|
||||
|
||||
mContext.startForegroundService(intent);
|
||||
|
||||
Logger.i(TAG, "Location sharing started, session ID: " + mSessionId);
|
||||
|
||||
return mShareUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop live location sharing.
|
||||
*/
|
||||
public void stopSharing()
|
||||
{
|
||||
if (!mIsSharing)
|
||||
{
|
||||
Logger.w(TAG, "Location sharing not active");
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop foreground service
|
||||
Intent intent = new Intent(mContext, LocationSharingService.class);
|
||||
mContext.stopService(intent);
|
||||
|
||||
mIsSharing = false;
|
||||
mSessionId = null;
|
||||
mEncryptionKey = null;
|
||||
mShareUrl = null;
|
||||
|
||||
Logger.i(TAG, "Location sharing stopped");
|
||||
}
|
||||
|
||||
public boolean isSharing()
|
||||
{
|
||||
return mIsSharing;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getShareUrl()
|
||||
{
|
||||
return mShareUrl;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSessionId()
|
||||
{
|
||||
return mSessionId;
|
||||
}
|
||||
|
||||
public void setUpdateIntervalSeconds(int seconds)
|
||||
{
|
||||
Config.LocationSharing.setUpdateInterval(seconds);
|
||||
}
|
||||
|
||||
public int getUpdateIntervalSeconds()
|
||||
{
|
||||
return Config.LocationSharing.getUpdateInterval();
|
||||
}
|
||||
|
||||
public void setServerBaseUrl(@NonNull String url)
|
||||
{
|
||||
Config.LocationSharing.setServerUrl(url);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getServerBaseUrl()
|
||||
{
|
||||
return Config.LocationSharing.getServerUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current battery level (0-100).
|
||||
*/
|
||||
public int getBatteryLevel()
|
||||
{
|
||||
BatteryManager bm = (BatteryManager) mContext.getSystemService(Context.BATTERY_SERVICE);
|
||||
if (bm == null)
|
||||
return 100;
|
||||
|
||||
return bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently navigating with an active route.
|
||||
*/
|
||||
public boolean isNavigating()
|
||||
{
|
||||
return RoutingController.get().isNavigating();
|
||||
}
|
||||
|
||||
// Native methods (implemented in JNI)
|
||||
|
||||
/**
|
||||
* Generate new session credentials (ID and encryption key).
|
||||
* @return Array of [sessionId, encryptionKey]
|
||||
*/
|
||||
@Nullable
|
||||
private static native String[] nativeGenerateSessionCredentials();
|
||||
|
||||
/**
|
||||
* Generate shareable URL from credentials.
|
||||
* @param sessionId Session ID (UUID)
|
||||
* @param encryptionKey Base64-encoded encryption key
|
||||
* @param serverBaseUrl Server base URL
|
||||
* @return Share URL
|
||||
*/
|
||||
@Nullable
|
||||
private static native String nativeGenerateShareUrl(String sessionId, String encryptionKey, String serverBaseUrl);
|
||||
|
||||
/**
|
||||
* Encrypt location payload.
|
||||
* @param encryptionKey Base64-encoded encryption key
|
||||
* @param payloadJson JSON payload to encrypt
|
||||
* @return Encrypted payload JSON (with iv, ciphertext, authTag) or null on failure
|
||||
*/
|
||||
@Nullable
|
||||
public static native String nativeEncryptPayload(String encryptionKey, String payloadJson);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package app.organicmaps.location;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import app.organicmaps.MwmActivity;
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.sdk.routing.RoutingInfo;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Helper for creating and updating location sharing notifications.
|
||||
*/
|
||||
public class LocationSharingNotification
|
||||
{
|
||||
public static final String CHANNEL_ID = "LOCATION_SHARING";
|
||||
private static final String CHANNEL_NAME = "Live Location Sharing";
|
||||
|
||||
private final Context mContext;
|
||||
private final NotificationManagerCompat mNotificationManager;
|
||||
|
||||
public LocationSharingNotification(@NonNull Context context)
|
||||
{
|
||||
mContext = context;
|
||||
mNotificationManager = NotificationManagerCompat.from(context);
|
||||
createNotificationChannel();
|
||||
}
|
||||
|
||||
private void createNotificationChannel()
|
||||
{
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
return;
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_LOW); // Low importance = no sound/vibration
|
||||
|
||||
channel.setDescription("Notifications for active live location sharing");
|
||||
channel.setShowBadge(false);
|
||||
channel.enableLights(false);
|
||||
channel.enableVibration(false);
|
||||
|
||||
NotificationManager nm = mContext.getSystemService(NotificationManager.class);
|
||||
if (nm != null)
|
||||
nm.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build notification for location sharing service.
|
||||
* @param stopIntent PendingIntent to stop sharing
|
||||
* @return Notification object
|
||||
*/
|
||||
@NonNull
|
||||
public Notification buildNotification(@NonNull PendingIntent stopIntent)
|
||||
{
|
||||
return buildNotification(stopIntent, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build notification with copy URL action.
|
||||
* @param stopIntent PendingIntent to stop sharing
|
||||
* @param copyUrlIntent PendingIntent to copy URL (optional)
|
||||
* @return Notification object
|
||||
*/
|
||||
@NonNull
|
||||
public Notification buildNotification(
|
||||
@NonNull PendingIntent stopIntent,
|
||||
@Nullable PendingIntent copyUrlIntent)
|
||||
{
|
||||
Intent notificationIntent = new Intent(mContext, MwmActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
mContext,
|
||||
0,
|
||||
notificationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_share)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setShowWhen(false)
|
||||
.setAutoCancel(false);
|
||||
|
||||
// Title
|
||||
builder.setContentTitle(mContext.getString(R.string.location_sharing_active));
|
||||
|
||||
// No subtitle - keep it simple
|
||||
|
||||
// Copy URL action button (if provided)
|
||||
if (copyUrlIntent != null)
|
||||
{
|
||||
builder.addAction(
|
||||
R.drawable.ic_share,
|
||||
mContext.getString(R.string.location_sharing_copy_url),
|
||||
copyUrlIntent);
|
||||
}
|
||||
|
||||
// Stop action button
|
||||
builder.addAction(
|
||||
R.drawable.ic_close,
|
||||
mContext.getString(R.string.location_sharing_stop),
|
||||
stopIntent);
|
||||
|
||||
// Set foreground service type for Android 10+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
{
|
||||
builder.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing notification.
|
||||
* @param notificationId Notification ID
|
||||
* @param notification Updated notification
|
||||
*/
|
||||
public void updateNotification(int notificationId, @NonNull Notification notification)
|
||||
{
|
||||
mNotificationManager.notify(notificationId, notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel notification.
|
||||
* @param notificationId Notification ID
|
||||
*/
|
||||
public void cancelNotification(int notificationId)
|
||||
{
|
||||
mNotificationManager.cancel(notificationId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
package app.organicmaps.location;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import app.organicmaps.MwmActivity;
|
||||
import app.organicmaps.MwmApplication;
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.api.LocationSharingApiClient;
|
||||
import app.organicmaps.sdk.location.LocationHelper;
|
||||
import app.organicmaps.sdk.location.LocationListener;
|
||||
import app.organicmaps.sdk.routing.RoutingController;
|
||||
import app.organicmaps.sdk.routing.RoutingInfo;
|
||||
import app.organicmaps.sdk.util.log.Logger;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Foreground service for live GPS location sharing.
|
||||
* Monitors location updates and posts encrypted data to server at regular intervals.
|
||||
*/
|
||||
public class LocationSharingService extends Service implements LocationListener
|
||||
{
|
||||
private static final String TAG = LocationSharingService.class.getSimpleName();
|
||||
private static final int NOTIFICATION_ID = 0x1002; // Unique ID for location sharing
|
||||
|
||||
// Intent extras
|
||||
public static final String EXTRA_SESSION_ID = "session_id";
|
||||
public static final String EXTRA_ENCRYPTION_KEY = "encryption_key";
|
||||
public static final String EXTRA_SERVER_URL = "server_url";
|
||||
public static final String EXTRA_UPDATE_INTERVAL = "update_interval";
|
||||
|
||||
// Actions for notification buttons
|
||||
private static final String ACTION_STOP = "app.organicmaps.ACTION_STOP_LOCATION_SHARING";
|
||||
private static final String ACTION_COPY_URL = "app.organicmaps.ACTION_COPY_LOCATION_URL";
|
||||
|
||||
@Nullable
|
||||
private String mSessionId;
|
||||
@Nullable
|
||||
private String mEncryptionKey;
|
||||
@Nullable
|
||||
private String mServerUrl;
|
||||
private int mUpdateIntervalSeconds = 20;
|
||||
|
||||
@Nullable
|
||||
private Location mLastLocation;
|
||||
private long mLastUpdateTimestamp = 0;
|
||||
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable mUpdateTask = this::processLocationUpdate;
|
||||
|
||||
@Nullable
|
||||
private LocationSharingApiClient mApiClient;
|
||||
@Nullable
|
||||
private LocationSharingNotification mNotificationHelper;
|
||||
|
||||
@Override
|
||||
public void onCreate()
|
||||
{
|
||||
super.onCreate();
|
||||
Logger.i(TAG, "Service created");
|
||||
|
||||
mNotificationHelper = new LocationSharingNotification(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(@Nullable Intent intent, int flags, int startId)
|
||||
{
|
||||
if (intent == null)
|
||||
{
|
||||
Logger.w(TAG, "Null intent, stopping service");
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// Handle stop action from notification
|
||||
if (ACTION_STOP.equals(intent.getAction()))
|
||||
{
|
||||
Logger.i(TAG, "Stop action received from notification");
|
||||
LocationSharingManager.getInstance().stopSharing();
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// Handle copy URL action from notification
|
||||
if (ACTION_COPY_URL.equals(intent.getAction()))
|
||||
{
|
||||
Logger.i(TAG, "Copy URL action received from notification");
|
||||
String shareUrl = LocationSharingManager.getInstance().getShareUrl();
|
||||
if (shareUrl != null)
|
||||
{
|
||||
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
|
||||
android.content.ClipData clip = android.content.ClipData.newPlainText("Location Share URL", shareUrl);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
android.widget.Toast.makeText(this, R.string.location_sharing_url_copied, android.widget.Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
// Extract session info
|
||||
mSessionId = intent.getStringExtra(EXTRA_SESSION_ID);
|
||||
mEncryptionKey = intent.getStringExtra(EXTRA_ENCRYPTION_KEY);
|
||||
mServerUrl = intent.getStringExtra(EXTRA_SERVER_URL);
|
||||
mUpdateIntervalSeconds = intent.getIntExtra(EXTRA_UPDATE_INTERVAL, 20);
|
||||
|
||||
if (mSessionId == null || mEncryptionKey == null || mServerUrl == null)
|
||||
{
|
||||
Logger.e(TAG, "Missing session info, stopping service");
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// Initialize API client
|
||||
mApiClient = new LocationSharingApiClient(mServerUrl, mSessionId);
|
||||
|
||||
// Create session on server
|
||||
mApiClient.createSession(new LocationSharingApiClient.Callback()
|
||||
{
|
||||
@Override
|
||||
public void onSuccess()
|
||||
{
|
||||
Logger.i(TAG, "Session created on server");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull String error)
|
||||
{
|
||||
Logger.w(TAG, "Failed to create session on server: " + error);
|
||||
}
|
||||
});
|
||||
|
||||
// Start foreground with notification
|
||||
Notification notification = mNotificationHelper != null
|
||||
? mNotificationHelper.buildNotification(getStopIntent(), getCopyUrlIntent())
|
||||
: buildFallbackNotification();
|
||||
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
|
||||
// Register for location updates
|
||||
LocationHelper locationHelper = MwmApplication.sInstance.getLocationHelper();
|
||||
locationHelper.addListener(this);
|
||||
|
||||
Logger.i(TAG, "Service started for session: " + mSessionId);
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy()
|
||||
{
|
||||
Logger.i(TAG, "Service destroyed");
|
||||
|
||||
// Unregister location listener
|
||||
LocationHelper locationHelper = MwmApplication.sInstance.getLocationHelper();
|
||||
locationHelper.removeListener(this);
|
||||
|
||||
// Cancel pending updates
|
||||
mHandler.removeCallbacks(mUpdateTask);
|
||||
|
||||
// Send session end to server (optional)
|
||||
if (mApiClient != null && mSessionId != null)
|
||||
mApiClient.endSession();
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent)
|
||||
{
|
||||
return null; // Not a bound service
|
||||
}
|
||||
|
||||
// LocationHelper.LocationListener implementation
|
||||
|
||||
@Override
|
||||
public void onLocationUpdated(@NonNull Location location)
|
||||
{
|
||||
mLastLocation = location;
|
||||
|
||||
// No need to update notification - it's simple and static now
|
||||
|
||||
// Schedule update if needed
|
||||
scheduleUpdate();
|
||||
}
|
||||
|
||||
|
||||
// Private methods
|
||||
|
||||
private void scheduleUpdate()
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
long timeSinceLastUpdate = (now - mLastUpdateTimestamp) / 1000; // Convert to seconds
|
||||
|
||||
if (timeSinceLastUpdate >= mUpdateIntervalSeconds)
|
||||
{
|
||||
// Remove any pending updates
|
||||
mHandler.removeCallbacks(mUpdateTask);
|
||||
// Execute immediately
|
||||
mHandler.post(mUpdateTask);
|
||||
}
|
||||
}
|
||||
|
||||
private void processLocationUpdate()
|
||||
{
|
||||
if (mLastLocation == null || mEncryptionKey == null || mApiClient == null)
|
||||
return;
|
||||
|
||||
// Check battery level
|
||||
int batteryLevel = getBatteryLevel();
|
||||
if (batteryLevel < 10)
|
||||
{
|
||||
Logger.w(TAG, "Battery level too low (" + batteryLevel + "%), stopping sharing");
|
||||
LocationSharingManager.getInstance().stopSharing();
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
|
||||
// Build payload JSON
|
||||
JSONObject payload = buildPayloadJson(mLastLocation, batteryLevel);
|
||||
if (payload == null)
|
||||
return;
|
||||
|
||||
// Encrypt payload
|
||||
String encryptedJson = LocationCrypto.encrypt(mEncryptionKey, payload.toString());
|
||||
if (encryptedJson == null)
|
||||
{
|
||||
Logger.e(TAG, "Failed to encrypt payload");
|
||||
return;
|
||||
}
|
||||
|
||||
// Send to server
|
||||
mApiClient.updateLocation(encryptedJson, new LocationSharingApiClient.Callback()
|
||||
{
|
||||
@Override
|
||||
public void onSuccess()
|
||||
{
|
||||
Logger.d(TAG, "Location update sent successfully");
|
||||
mLastUpdateTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull String error)
|
||||
{
|
||||
Logger.w(TAG, "Failed to send location update: " + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private JSONObject buildPayloadJson(@NonNull Location location, int batteryLevel)
|
||||
{
|
||||
try
|
||||
{
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("timestamp", System.currentTimeMillis() / 1000); // Unix timestamp
|
||||
json.put("lat", location.getLatitude());
|
||||
json.put("lon", location.getLongitude());
|
||||
json.put("accuracy", location.getAccuracy());
|
||||
|
||||
if (location.hasSpeed())
|
||||
json.put("speed", location.getSpeed());
|
||||
|
||||
if (location.hasBearing())
|
||||
json.put("bearing", location.getBearing());
|
||||
|
||||
// Check if navigating
|
||||
RoutingInfo routingInfo = getNavigationInfo();
|
||||
if (routingInfo != null && routingInfo.distToTarget != null)
|
||||
{
|
||||
json.put("mode", "navigation");
|
||||
|
||||
// Calculate ETA (current time + time remaining)
|
||||
if (routingInfo.totalTimeInSeconds > 0)
|
||||
{
|
||||
long etaTimestamp = (System.currentTimeMillis() / 1000) + routingInfo.totalTimeInSeconds;
|
||||
json.put("eta", etaTimestamp);
|
||||
}
|
||||
|
||||
// Distance remaining in meters
|
||||
if (routingInfo.distToTarget != null)
|
||||
{
|
||||
json.put("distanceRemaining", routingInfo.distToTarget.mDistance);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
json.put("mode", "standalone");
|
||||
}
|
||||
|
||||
json.put("batteryLevel", batteryLevel);
|
||||
|
||||
return json;
|
||||
}
|
||||
catch (JSONException e)
|
||||
{
|
||||
Logger.e(TAG, "Failed to build payload JSON", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private RoutingInfo getNavigationInfo()
|
||||
{
|
||||
if (!RoutingController.get().isNavigating())
|
||||
return null;
|
||||
|
||||
return RoutingController.get().getCachedRoutingInfo();
|
||||
}
|
||||
|
||||
private int getBatteryLevel()
|
||||
{
|
||||
BatteryManager bm = (BatteryManager) getSystemService(BATTERY_SERVICE);
|
||||
if (bm == null)
|
||||
return 100;
|
||||
|
||||
return bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private PendingIntent getStopIntent()
|
||||
{
|
||||
Intent stopIntent = new Intent(this, LocationSharingService.class);
|
||||
stopIntent.setAction(ACTION_STOP);
|
||||
return PendingIntent.getService(this, 0, stopIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private PendingIntent getCopyUrlIntent()
|
||||
{
|
||||
Intent copyIntent = new Intent(this, LocationSharingService.class);
|
||||
copyIntent.setAction(ACTION_COPY_URL);
|
||||
return PendingIntent.getService(this, 1, copyIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Notification buildFallbackNotification()
|
||||
{
|
||||
Intent notificationIntent = new Intent(this, MwmActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
return new NotificationCompat.Builder(this, LocationSharingNotification.CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.location_sharing_active))
|
||||
.setSmallIcon(R.drawable.ic_share)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -322,7 +322,8 @@ public class MapButtonsController extends Fragment
|
||||
mBadgeDrawable.setVisible(count > 0);
|
||||
BadgeUtils.attachBadgeDrawable(mBadgeDrawable, menuButton);
|
||||
|
||||
updateMenuBadge(TrackRecorder.nativeIsTrackRecordingEnabled());
|
||||
final boolean isTrackRecording = TrackRecorder.nativeIsTrackRecordingEnabled();
|
||||
updateMenuBadge(isTrackRecording);
|
||||
}
|
||||
|
||||
public void updateLayerButton()
|
||||
|
||||
@@ -16,6 +16,7 @@ public class MapButtonsViewModel extends ViewModel
|
||||
private final MutableLiveData<SearchWheel.SearchOption> mSearchOption = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> mTrackRecorderState =
|
||||
new MutableLiveData<>(TrackRecorder.nativeIsTrackRecordingEnabled());
|
||||
private final MutableLiveData<Boolean> mLocationSharingState = new MutableLiveData<>(false);
|
||||
|
||||
public MutableLiveData<Boolean> getButtonsHidden()
|
||||
{
|
||||
@@ -86,4 +87,14 @@ public class MapButtonsViewModel extends ViewModel
|
||||
{
|
||||
return mTrackRecorderState;
|
||||
}
|
||||
|
||||
public void setLocationSharingState(boolean state)
|
||||
{
|
||||
mLocationSharingState.setValue(state);
|
||||
}
|
||||
|
||||
public MutableLiveData<Boolean> getLocationSharingState()
|
||||
{
|
||||
return mLocationSharingState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +205,11 @@ public class NavigationController implements TrafficManager.TrafficCallback, Nav
|
||||
mNavMenu.refreshTts();
|
||||
}
|
||||
|
||||
public void refreshShareLocationColor()
|
||||
{
|
||||
mNavMenu.updateShareLocationColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnabled()
|
||||
{
|
||||
|
||||
@@ -26,8 +26,10 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import app.organicmaps.MwmActivity;
|
||||
import app.organicmaps.MwmApplication;
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.location.LocationSharingDialog;
|
||||
import app.organicmaps.sdk.Framework;
|
||||
import app.organicmaps.sdk.bookmarks.data.DistanceAndAzimut;
|
||||
import app.organicmaps.sdk.routing.RouteMarkData;
|
||||
@@ -144,6 +146,9 @@ final class RoutingBottomMenuController implements View.OnClickListener
|
||||
mActionButton.setOnClickListener(this);
|
||||
View actionSearchButton = actionFrame.findViewById(R.id.btn__search_point);
|
||||
actionSearchButton.setOnClickListener(this);
|
||||
View shareLocationButton = actionFrame.findViewById(R.id.btn__share_location);
|
||||
if (shareLocationButton != null)
|
||||
shareLocationButton.setOnClickListener(this);
|
||||
mActionIcon = mActionButton.findViewById(R.id.iv__icon);
|
||||
UiUtils.hide(mAltitudeChartFrame, mActionFrame);
|
||||
mListener = listener;
|
||||
@@ -472,6 +477,11 @@ final class RoutingBottomMenuController implements View.OnClickListener
|
||||
final RouteMarkType pointType = (RouteMarkType) mActionMessage.getTag();
|
||||
mListener.onSearchRoutePoint(pointType);
|
||||
}
|
||||
else if (id == R.id.btn__share_location)
|
||||
{
|
||||
if (mContext instanceof MwmActivity)
|
||||
LocationSharingDialog.show(((MwmActivity) mContext).getSupportFragmentManager());
|
||||
}
|
||||
else if (id == R.id.btn__manage_route)
|
||||
mListener.onManageRouteOpen();
|
||||
else if (id == R.id.btn__save)
|
||||
|
||||
@@ -273,7 +273,7 @@ public class SearchFragment extends BaseMwmFragment implements SearchListener, C
|
||||
RecyclerView mResults = mResultsFrame.findViewById(R.id.recycler);
|
||||
setRecyclerScrollListener(mResults);
|
||||
mResultsPlaceholder = mResultsFrame.findViewById(R.id.placeholder);
|
||||
mResultsPlaceholder.setContent(R.string.search_not_found, R.string.search_not_found_query, R.drawable.ic_search_fail);
|
||||
mResultsPlaceholder.setContent(R.string.search_not_found, R.string.search_not_found_query);
|
||||
mSearchAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver()
|
||||
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ public class SearchHistoryFragment extends BaseMwmRecyclerFragment<SearchHistory
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
getRecyclerView().setLayoutManager(new LinearLayoutManager(view.getContext()));
|
||||
mPlaceHolder = view.findViewById(R.id.placeholder);
|
||||
mPlaceHolder.setContent(R.string.search_history_title, R.string.search_history_text, R.drawable.ic_search_recent);
|
||||
mPlaceHolder.setContent(R.string.search_history_title, R.string.search_history_text);
|
||||
|
||||
getAdapter().registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
||||
@Override
|
||||
|
||||
@@ -90,36 +90,28 @@ public class DrivingOptionsFragment extends BaseMwmToolbarFragment
|
||||
{
|
||||
SwitchCompat tollsBtn = root.findViewById(R.id.avoid_tolls_btn);
|
||||
tollsBtn.setChecked(RoutingOptions.hasOption(RoadType.Toll));
|
||||
CompoundButton.OnCheckedChangeListener tollBtnListener = new ToggleRoutingOptionListener(RoadType.Toll, root);
|
||||
CompoundButton.OnCheckedChangeListener tollBtnListener = new ToggleRoutingOptionListener(RoadType.Toll);
|
||||
tollsBtn.setOnCheckedChangeListener(tollBtnListener);
|
||||
|
||||
SwitchCompat motorwaysBtn = root.findViewById(R.id.avoid_motorways_btn);
|
||||
motorwaysBtn.setChecked(RoutingOptions.hasOption(RoadType.Motorway));
|
||||
CompoundButton.OnCheckedChangeListener motorwayBtnListener =
|
||||
new ToggleRoutingOptionListener(RoadType.Motorway, root);
|
||||
CompoundButton.OnCheckedChangeListener motorwayBtnListener = new ToggleRoutingOptionListener(RoadType.Motorway);
|
||||
motorwaysBtn.setOnCheckedChangeListener(motorwayBtnListener);
|
||||
|
||||
SwitchCompat ferriesBtn = root.findViewById(R.id.avoid_ferries_btn);
|
||||
ferriesBtn.setChecked(RoutingOptions.hasOption(RoadType.Ferry));
|
||||
CompoundButton.OnCheckedChangeListener ferryBtnListener = new ToggleRoutingOptionListener(RoadType.Ferry, root);
|
||||
CompoundButton.OnCheckedChangeListener ferryBtnListener = new ToggleRoutingOptionListener(RoadType.Ferry);
|
||||
ferriesBtn.setOnCheckedChangeListener(ferryBtnListener);
|
||||
|
||||
SwitchCompat dirtyRoadsBtn = root.findViewById(R.id.avoid_dirty_roads_btn);
|
||||
dirtyRoadsBtn.setChecked(RoutingOptions.hasOption(RoadType.Dirty));
|
||||
dirtyRoadsBtn.setEnabled(!RoutingOptions.hasOption(RoadType.Paved) || RoutingOptions.hasOption(RoadType.Dirty));
|
||||
CompoundButton.OnCheckedChangeListener dirtyBtnListener = new ToggleRoutingOptionListener(RoadType.Dirty, root);
|
||||
CompoundButton.OnCheckedChangeListener dirtyBtnListener = new ToggleRoutingOptionListener(RoadType.Dirty);
|
||||
dirtyRoadsBtn.setOnCheckedChangeListener(dirtyBtnListener);
|
||||
|
||||
SwitchCompat stepsBtn = root.findViewById(R.id.avoid_steps_btn);
|
||||
stepsBtn.setChecked(RoutingOptions.hasOption(RoadType.Steps));
|
||||
CompoundButton.OnCheckedChangeListener stepsBtnListener = new ToggleRoutingOptionListener(RoadType.Steps, root);
|
||||
CompoundButton.OnCheckedChangeListener stepsBtnListener = new ToggleRoutingOptionListener(RoadType.Steps);
|
||||
stepsBtn.setOnCheckedChangeListener(stepsBtnListener);
|
||||
|
||||
SwitchCompat pavedBtn = root.findViewById(R.id.avoid_paved_roads_btn);
|
||||
pavedBtn.setChecked(RoutingOptions.hasOption(RoadType.Paved));
|
||||
pavedBtn.setEnabled(!RoutingOptions.hasOption(RoadType.Dirty) || RoutingOptions.hasOption(RoadType.Paved));
|
||||
CompoundButton.OnCheckedChangeListener pavedBtnListener = new ToggleRoutingOptionListener(RoadType.Paved, root);
|
||||
pavedBtn.setOnCheckedChangeListener(pavedBtnListener);
|
||||
}
|
||||
|
||||
private static class ToggleRoutingOptionListener implements CompoundButton.OnCheckedChangeListener
|
||||
@@ -127,13 +119,9 @@ public class DrivingOptionsFragment extends BaseMwmToolbarFragment
|
||||
@NonNull
|
||||
private final RoadType mRoadType;
|
||||
|
||||
@NonNull
|
||||
private final View mRoot;
|
||||
|
||||
private ToggleRoutingOptionListener(@NonNull RoadType roadType, @NonNull View root)
|
||||
private ToggleRoutingOptionListener(@NonNull RoadType roadType)
|
||||
{
|
||||
mRoadType = roadType;
|
||||
mRoot = root;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -143,27 +131,6 @@ public class DrivingOptionsFragment extends BaseMwmToolbarFragment
|
||||
RoutingOptions.addOption(mRoadType);
|
||||
else
|
||||
RoutingOptions.removeOption(mRoadType);
|
||||
|
||||
SwitchCompat dirtyRoadsBtn = mRoot.findViewById(R.id.avoid_dirty_roads_btn);
|
||||
SwitchCompat pavedBtn = mRoot.findViewById(R.id.avoid_paved_roads_btn);
|
||||
if (mRoadType == RoadType.Dirty)
|
||||
{
|
||||
pavedBtn.setEnabled(!isChecked);
|
||||
if (isChecked)
|
||||
{
|
||||
pavedBtn.setChecked(false);
|
||||
dirtyRoadsBtn.setEnabled(true);
|
||||
}
|
||||
}
|
||||
else if (mRoadType == RoadType.Paved)
|
||||
{
|
||||
dirtyRoadsBtn.setEnabled(!isChecked);
|
||||
if (isChecked)
|
||||
{
|
||||
dirtyRoadsBtn.setChecked(false);
|
||||
pavedBtn.setEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.EditTextPreference;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
@@ -73,6 +74,7 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
|
||||
initScreenSleepEnabledPrefsCallbacks();
|
||||
initShowOnLockScreenPrefsCallbacks();
|
||||
initLeftButtonPrefs();
|
||||
initLocationSharingPrefsCallbacks();
|
||||
}
|
||||
|
||||
private void initLeftButtonPrefs()
|
||||
@@ -542,6 +544,29 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
|
||||
category.removePreference(preference);
|
||||
}
|
||||
|
||||
private void initLocationSharingPrefsCallbacks()
|
||||
{
|
||||
// Server URL preference
|
||||
final EditTextPreference serverUrlPref = getPreference(getString(R.string.pref_location_sharing_server_url));
|
||||
serverUrlPref.setText(Config.LocationSharing.getServerUrl());
|
||||
serverUrlPref.setSummary(Config.LocationSharing.getServerUrl());
|
||||
serverUrlPref.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
String url = (String) newValue;
|
||||
Config.LocationSharing.setServerUrl(url);
|
||||
serverUrlPref.setSummary(url);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Update interval preference
|
||||
final ListPreference intervalPref = getPreference(getString(R.string.pref_location_sharing_update_interval));
|
||||
intervalPref.setValue(String.valueOf(Config.LocationSharing.getUpdateInterval()));
|
||||
intervalPref.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
int seconds = Integer.parseInt((String) newValue);
|
||||
Config.LocationSharing.setUpdateInterval(seconds);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLanguageSelected(Language language)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@ package app.organicmaps.widget;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -12,7 +11,6 @@ import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.material.imageview.ShapeableImageView;
|
||||
import com.google.android.material.textview.MaterialTextView;
|
||||
@@ -178,10 +176,9 @@ public class PlaceholderView extends LinearLayout
|
||||
return view.getMeasuredHeight() + params.bottomMargin + params.topMargin;
|
||||
}
|
||||
|
||||
public void setContent(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes)
|
||||
public void setContent(@StringRes int titleRes, @StringRes int subtitleRes)
|
||||
{
|
||||
mTitle.setText(titleRes);
|
||||
mSubtitle.setText(subtitleRes);
|
||||
mImage.setImageDrawable(ContextCompat.getDrawable(getContext(), iconRes));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.location.LocationSharingDialog;
|
||||
import app.organicmaps.sdk.routing.RoutingInfo;
|
||||
import app.organicmaps.sdk.sound.TtsPlayer;
|
||||
import app.organicmaps.sdk.util.DateUtils;
|
||||
@@ -26,6 +27,7 @@ public class NavMenu
|
||||
private final View mHeaderFrame;
|
||||
|
||||
private final ShapeableImageView mTts;
|
||||
private final ShapeableImageView mShareLocation;
|
||||
private final MaterialTextView mEtaValue;
|
||||
private final MaterialTextView mEtaAmPm;
|
||||
private final MaterialTextView mTimeHourValue;
|
||||
@@ -97,12 +99,16 @@ public class NavMenu
|
||||
mRouteProgress = bottomFrame.findViewById(R.id.navigation_progress);
|
||||
|
||||
// Bottom frame buttons
|
||||
mShareLocation = bottomFrame.findViewById(R.id.share_location);
|
||||
mShareLocation.setOnClickListener(v -> onShareLocationClicked());
|
||||
ShapeableImageView mSettings = bottomFrame.findViewById(R.id.settings);
|
||||
mSettings.setOnClickListener(v -> onSettingsClicked());
|
||||
mTts = bottomFrame.findViewById(R.id.tts_volume);
|
||||
mTts.setOnClickListener(v -> onTtsClicked());
|
||||
MaterialButton stop = bottomFrame.findViewById(R.id.stop);
|
||||
stop.setOnClickListener(v -> onStopClicked());
|
||||
|
||||
updateShareLocationColor();
|
||||
}
|
||||
|
||||
private void onStopClicked()
|
||||
@@ -110,6 +116,22 @@ public class NavMenu
|
||||
mNavMenuListener.onStopClicked();
|
||||
}
|
||||
|
||||
private void onShareLocationClicked()
|
||||
{
|
||||
LocationSharingDialog.show(mActivity.getSupportFragmentManager());
|
||||
// Update color after dialog is shown (in case state changes)
|
||||
mShareLocation.postDelayed(this::updateShareLocationColor, 500);
|
||||
}
|
||||
|
||||
public void updateShareLocationColor()
|
||||
{
|
||||
final boolean isLocationSharing = app.organicmaps.location.LocationSharingManager.getInstance().isSharing();
|
||||
final int color = isLocationSharing
|
||||
? androidx.core.content.ContextCompat.getColor(mActivity, R.color.active_location_sharing)
|
||||
: app.organicmaps.util.ThemeUtils.getColor(mActivity, R.attr.iconTint);
|
||||
mShareLocation.setImageTintList(android.content.res.ColorStateList.valueOf(color));
|
||||
}
|
||||
|
||||
private void onSettingsClicked()
|
||||
{
|
||||
mNavMenuListener.onSettingsClicked();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package app.organicmaps.widget.placepage;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
@@ -15,7 +14,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentFactory;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import app.organicmaps.R;
|
||||
@@ -105,9 +103,9 @@ public class EditBookmarkFragment extends BaseMwmDialogFragment implements View.
|
||||
public EditBookmarkFragment() {}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.MwmTheme_FullScreenDialog);
|
||||
protected int getCustomTheme()
|
||||
{
|
||||
return getFullscreenTheme();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -183,12 +181,6 @@ public class EditBookmarkFragment extends BaseMwmDialogFragment implements View.
|
||||
public void onStart()
|
||||
{
|
||||
super.onStart();
|
||||
Dialog dialog = getDialog();
|
||||
if (dialog != null) {
|
||||
dialog.getWindow().setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
}
|
||||
|
||||
// Focus name and show keyboard for "Unknown Place" bookmarks
|
||||
if (mBookmark != null
|
||||
|
||||
@@ -152,7 +152,7 @@ public class PlacePageView extends Fragment
|
||||
private View mEditTopSpace;
|
||||
private ShapeableImageView mColorIcon;
|
||||
private MaterialTextView mTvCategory;
|
||||
private MaterialButton mEditBookmark;
|
||||
private ShapeableImageView mEditBookmark;
|
||||
|
||||
// Data
|
||||
private CoordinatesFormat mCoordsFormat = CoordinatesFormat.LatLonDecimal;
|
||||
@@ -691,27 +691,16 @@ public class PlacePageView extends Fragment
|
||||
mTvAddPlace.setOnClickListener(this);
|
||||
mTvEditPlace.setEnabled(Editor.nativeShouldEnableEditPlace());
|
||||
mTvAddPlace.setEnabled(Editor.nativeShouldEnableAddPlace());
|
||||
final int editTextButtonColor =
|
||||
final int editPlaceButtonColor =
|
||||
Editor.nativeShouldEnableEditPlace()
|
||||
? ContextCompat.getColor(
|
||||
getContext(),
|
||||
UiUtils.getStyledResourceId(getContext(), com.google.android.material.R.attr.colorSecondary))
|
||||
: ContextCompat.getColor(getContext(), R.color.button_accent_text_disabled);
|
||||
final ColorStateList editStrokeButtonColor = new ColorStateList(
|
||||
new int[][]{
|
||||
new int[]{android.R.attr.state_enabled}, // enabled
|
||||
new int[]{-android.R.attr.state_enabled} // disabled
|
||||
},
|
||||
new int[]{
|
||||
ContextCompat.getColor(
|
||||
getContext(),
|
||||
UiUtils.getStyledResourceId(getContext(), com.google.android.material.R.attr.colorSecondary)),
|
||||
ContextCompat.getColor(getContext(), R.color.button_accent_text_disabled)
|
||||
});
|
||||
mTvEditPlace.setTextColor(editTextButtonColor);
|
||||
mTvAddPlace.setTextColor(editTextButtonColor);
|
||||
mTvEditPlace.setStrokeColor(editStrokeButtonColor);
|
||||
mTvAddPlace.setStrokeColor(editStrokeButtonColor);
|
||||
mTvEditPlace.setTextColor(editPlaceButtonColor);
|
||||
mTvAddPlace.setTextColor(editPlaceButtonColor);
|
||||
mTvEditPlace.setStrokeColor(ColorStateList.valueOf(editPlaceButtonColor));
|
||||
mTvAddPlace.setStrokeColor(ColorStateList.valueOf(editPlaceButtonColor));
|
||||
UiUtils.showIf(
|
||||
UiUtils.isVisible(mEditPlace) || UiUtils.isVisible(mAddPlace),
|
||||
mEditTopSpace);
|
||||
|
||||
@@ -59,6 +59,8 @@ public class PlacePageBookmarkFragment extends Fragment implements View.OnClickL
|
||||
mFrame = view;
|
||||
mTvBookmarkNote = mFrame.findViewById(R.id.tv__bookmark_notes);
|
||||
mTvBookmarkNote.setOnLongClickListener(this);
|
||||
final View editBookmarkBtn = mFrame.findViewById(R.id.tv__bookmark_edit);
|
||||
editBookmarkBtn.setOnClickListener(this);
|
||||
}
|
||||
|
||||
private void initWebView()
|
||||
|
||||
BIN
android/app/src/main/res/drawable-hdpi/ic_avoid_ferry.webp
Normal file
|
After Width: | Height: | Size: 520 B |
BIN
android/app/src/main/res/drawable-hdpi/ic_avoid_tolls.webp
Normal file
|
After Width: | Height: | Size: 620 B |
BIN
android/app/src/main/res/drawable-hdpi/ic_avoid_unpaved.webp
Normal file
|
After Width: | Height: | Size: 594 B |
BIN
android/app/src/main/res/drawable-hdpi/ic_triangle.webp
Normal file
|
After Width: | Height: | Size: 86 B |
|
After Width: | Height: | Size: 88 B |
BIN
android/app/src/main/res/drawable-hdpi/img_empty_bookmarks.webp
Normal file
|
After Width: | Height: | Size: 42 B |
BIN
android/app/src/main/res/drawable-hdpi/img_search_no_maps.webp
Normal file
|
After Width: | Height: | Size: 44 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_avoid_ferry.webp
Normal file
|
After Width: | Height: | Size: 338 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_avoid_tolls.webp
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_avoid_unpaved.webp
Normal file
|
After Width: | Height: | Size: 402 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_triangle.webp
Normal file
|
After Width: | Height: | Size: 64 B |
|
After Width: | Height: | Size: 70 B |
BIN
android/app/src/main/res/drawable-mdpi/img_empty_bookmarks.webp
Normal file
|
After Width: | Height: | Size: 44 B |
BIN
android/app/src/main/res/drawable-mdpi/img_search_no_maps.webp
Normal file
|
After Width: | Height: | Size: 42 B |
BIN
android/app/src/main/res/drawable-night-hdpi/ic_triangle.webp
Normal file
|
After Width: | Height: | Size: 86 B |
|
After Width: | Height: | Size: 86 B |
BIN
android/app/src/main/res/drawable-night-mdpi/ic_triangle.webp
Normal file
|
After Width: | Height: | Size: 72 B |
|
After Width: | Height: | Size: 68 B |
BIN
android/app/src/main/res/drawable-night-xhdpi/ic_triangle.webp
Normal file
|
After Width: | Height: | Size: 94 B |
|
After Width: | Height: | Size: 92 B |
BIN
android/app/src/main/res/drawable-night-xxhdpi/ic_triangle.webp
Normal file
|
After Width: | Height: | Size: 130 B |
|
After Width: | Height: | Size: 112 B |
BIN
android/app/src/main/res/drawable-night-xxxhdpi/ic_triangle.webp
Normal file
|
After Width: | Height: | Size: 134 B |
|
After Width: | Height: | Size: 110 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_avoid_ferry.webp
Normal file
|
After Width: | Height: | Size: 690 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_avoid_tolls.webp
Normal file
|
After Width: | Height: | Size: 826 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_avoid_unpaved.webp
Normal file
|
After Width: | Height: | Size: 814 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_triangle.webp
Normal file
|
After Width: | Height: | Size: 94 B |
|
After Width: | Height: | Size: 100 B |
BIN
android/app/src/main/res/drawable-xhdpi/img_empty_bookmarks.webp
Normal file
|
After Width: | Height: | Size: 46 B |
BIN
android/app/src/main/res/drawable-xhdpi/img_search_no_maps.webp
Normal file
|
After Width: | Height: | Size: 50 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_avoid_ferry.webp
Normal file
|
After Width: | Height: | Size: 1016 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_avoid_tolls.webp
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_avoid_unpaved.webp
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_triangle.webp
Normal file
|
After Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 132 B |
|
After Width: | Height: | Size: 54 B |
BIN
android/app/src/main/res/drawable-xxhdpi/img_search_no_maps.webp
Normal file
|
After Width: | Height: | Size: 60 B |