mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Workout: offline map init
This commit is contained in:
@@ -248,6 +248,13 @@ dependencies {
|
|||||||
implementation 'com.android.volley:volley:1.2.1'
|
implementation 'com.android.volley:volley:1.2.1'
|
||||||
implementation 'org.msgpack:msgpack-core:0.9.9'
|
implementation 'org.msgpack:msgpack-core:0.9.9'
|
||||||
|
|
||||||
|
implementation 'com.github.mapsforge.mapsforge:mapsforge-core:0.21.0'
|
||||||
|
implementation 'com.github.mapsforge.mapsforge:mapsforge-map:0.21.0'
|
||||||
|
implementation 'com.github.mapsforge.mapsforge:mapsforge-map-reader:0.21.0'
|
||||||
|
implementation 'com.github.mapsforge.mapsforge:mapsforge-themes:0.21.0'
|
||||||
|
implementation 'com.github.mapsforge.mapsforge:mapsforge-map-android:0.21.0'
|
||||||
|
implementation 'com.caverock:androidsvg:1.4'
|
||||||
|
|
||||||
// Bouncy Castle is included directly in GB, to avoid pulling the entire dependency
|
// Bouncy Castle is included directly in GB, to avoid pulling the entire dependency
|
||||||
// It's included in the org.bouncycastle.shaded package, to fix conflicts with roboelectric
|
// It's included in the org.bouncycastle.shaded package, to fix conflicts with roboelectric
|
||||||
//implementation 'org.bouncycastle:bcpkix-jdk18on:1.76'
|
//implementation 'org.bouncycastle:bcpkix-jdk18on:1.76'
|
||||||
|
|||||||
@@ -173,6 +173,14 @@
|
|||||||
android:name=".activities.DashboardPreferencesActivity"
|
android:name=".activities.DashboardPreferencesActivity"
|
||||||
android:label="@string/dashboard_settings"
|
android:label="@string/dashboard_settings"
|
||||||
android:parentActivityName=".activities.SettingsActivity" />
|
android:parentActivityName=".activities.SettingsActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.maps.MapsTrackActivity"
|
||||||
|
android:label="@string/maps"
|
||||||
|
android:parentActivityName=".activities.SettingsActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.maps.MapsSettingsActivity"
|
||||||
|
android:label="@string/maps_settings"
|
||||||
|
android:parentActivityName=".activities.SettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SleepAsAndroidPreferencesActivity"
|
android:name=".activities.SleepAsAndroidPreferencesActivity"
|
||||||
android:label="@string/sleepasandroid_settings"
|
android:label="@string/sleepasandroid_settings"
|
||||||
|
|||||||
+75
-68
@@ -16,9 +16,9 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -27,6 +27,11 @@ import android.widget.ImageView;
|
|||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.mapsforge.core.graphics.Paint;
|
||||||
|
import org.mapsforge.core.model.LatLong;
|
||||||
|
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
|
||||||
|
import org.mapsforge.map.android.view.MapView;
|
||||||
|
import org.mapsforge.map.layer.overlay.Polyline;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -38,9 +43,11 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsTrackActivity;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
|
||||||
@@ -48,21 +55,28 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDat
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParseException;
|
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParseException;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser;
|
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.maps.MapsManager;
|
||||||
|
|
||||||
import static android.graphics.Bitmap.createBitmap;
|
import static android.graphics.Bitmap.createBitmap;
|
||||||
|
|
||||||
|
|
||||||
public class ActivitySummariesGpsFragment extends AbstractGBFragment {
|
public class ActivitySummariesGpsFragment extends AbstractGBFragment {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummariesGpsFragment.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummariesGpsFragment.class);
|
||||||
private ImageView gpsView;
|
|
||||||
private final int CANVAS_SIZE = 360;
|
private final int CANVAS_SIZE = 360;
|
||||||
|
private MapView mapView;
|
||||||
private File inputFile;
|
private File inputFile;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
View rootView = inflater.inflate(R.layout.fragment_gps, container, false);
|
View rootView = inflater.inflate(R.layout.fragment_gps, container, false);
|
||||||
gpsView = rootView.findViewById(R.id.activitygpsview);
|
mapView = rootView.findViewById(R.id.activitygpsview);
|
||||||
|
//mapView.getMapScaleBar().setVisible(true);
|
||||||
|
mapView.setBuiltInZoomControls(false);
|
||||||
|
|
||||||
|
MapsManager mapsManager = new MapsManager(requireContext());
|
||||||
|
mapsManager.loadMaps(mapView);
|
||||||
|
|
||||||
if (inputFile != null) {
|
if (inputFile != null) {
|
||||||
processInBackgroundThread();
|
processInBackgroundThread();
|
||||||
}
|
}
|
||||||
@@ -71,88 +85,83 @@ public class ActivitySummariesGpsFragment extends AbstractGBFragment {
|
|||||||
|
|
||||||
public void set_data(File inputFile) {
|
public void set_data(File inputFile) {
|
||||||
this.inputFile = inputFile;
|
this.inputFile = inputFile;
|
||||||
if (gpsView != null) { //first fragment inflate is AFTER this is called
|
if (mapView != null) { //first fragment inflate is AFTER this is called
|
||||||
processInBackgroundThread();
|
processInBackgroundThread();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processInBackgroundThread() {
|
private void processInBackgroundThread() {
|
||||||
final Canvas canvas = createCanvas(gpsView);
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
final List<GPSCoordinate> points = getActivityPoints(inputFile)
|
final ArrayList<GPSCoordinate> points = new ArrayList<>();
|
||||||
.stream()
|
if (inputFile.getName().endsWith(".gpx")) {
|
||||||
.map(ActivityPoint::getLocation)
|
try (FileInputStream inputStream = new FileInputStream(inputFile)) {
|
||||||
.filter(Objects::nonNull)
|
final GpxParser gpxParser = new GpxParser(inputStream);
|
||||||
.collect(Collectors.toList());
|
points.addAll(gpxParser.getGpxFile().getPoints());
|
||||||
|
} catch (final IOException e) {
|
||||||
|
LOG.error("Failed to open {}", inputFile, e);
|
||||||
|
return;
|
||||||
|
} catch (final GpxParseException e) {
|
||||||
|
LOG.error("Failed to parse gpx file", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (inputFile.getName().endsWith(".fit")) {
|
||||||
|
try {
|
||||||
|
FitFile fitFile = FitFile.parseIncoming(inputFile);
|
||||||
|
for (final RecordData record : fitFile.getRecords()) {
|
||||||
|
if (record instanceof FitRecord) {
|
||||||
|
final ActivityPoint activityPoint = ((FitRecord) record).toActivityPoint();
|
||||||
|
if (activityPoint.getLocation() != null) {
|
||||||
|
points.add(activityPoint.getLocation());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (final IOException e) {
|
||||||
|
LOG.error("Failed to open {}", inputFile, e);
|
||||||
|
return;
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("Failed to parse fit file", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG.warn("Unknown file type {}", inputFile.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!points.isEmpty()) {
|
if (!points.isEmpty()) {
|
||||||
drawTrack(canvas, points);
|
drawTrack(mapView, points);
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<ActivityPoint> getActivityPoints(final File trackFile) {
|
private void drawTrack(MapView mapView, final ArrayList<? extends GPSCoordinate> trackPoints) {
|
||||||
final List<ActivityPoint> points = new ArrayList<>();
|
|
||||||
if (trackFile == null) {
|
|
||||||
return points;
|
|
||||||
}
|
|
||||||
if (trackFile.getName().endsWith(".gpx")) {
|
|
||||||
try (FileInputStream inputStream = new FileInputStream(trackFile)) {
|
|
||||||
final GpxParser gpxParser = new GpxParser(inputStream);
|
|
||||||
points.addAll(gpxParser.getGpxFile().getActivityPoints());
|
|
||||||
} catch (final IOException e) {
|
|
||||||
LOG.error("Failed to open {}", trackFile, e);
|
|
||||||
} catch (final GpxParseException e) {
|
|
||||||
LOG.error("Failed to parse gpx file", e);
|
|
||||||
}
|
|
||||||
} else if (trackFile.getName().endsWith(".fit")) {
|
|
||||||
try {
|
|
||||||
FitFile fitFile = FitFile.parseIncoming(trackFile);
|
|
||||||
for (final RecordData record : fitFile.getRecords()) {
|
|
||||||
if (record instanceof FitRecord) {
|
|
||||||
points.add(((FitRecord) record).toActivityPoint());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (final IOException e) {
|
|
||||||
LOG.error("Failed to open {}", trackFile, e);
|
|
||||||
} catch (final Exception e) {
|
|
||||||
LOG.error("Failed to parse fit file", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG.warn("Unknown file type {}", trackFile.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
return points;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void drawTrack(Canvas canvas, List<? extends GPSCoordinate> trackPoints) {
|
|
||||||
double maxLat = (Collections.max(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude();
|
double maxLat = (Collections.max(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude();
|
||||||
double minLat = (Collections.min(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude();
|
double minLat = (Collections.min(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude();
|
||||||
double maxLon = (Collections.max(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude();
|
double maxLon = (Collections.max(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude();
|
||||||
double minLon = (Collections.min(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude();
|
double minLon = (Collections.min(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude();
|
||||||
double maxAlt = (Collections.max(trackPoints, new GPSCoordinate.compareElevation())).getAltitude();
|
List<LatLong> latlongs = trackPoints.stream()
|
||||||
double minAlt = (Collections.min(trackPoints, new GPSCoordinate.compareElevation())).getAltitude();
|
.map(p -> new LatLong(p.getLatitude(), p.getLongitude()))
|
||||||
float scale_factor_w = (float) ((maxLon - minLon) / (maxLat - minLat));
|
.collect(Collectors.toList());
|
||||||
float scale_factor_h = (float) ((maxLat - minLat) / (maxLon - minLon));
|
|
||||||
|
|
||||||
if (scale_factor_h > scale_factor_w) { //scaling to draw proportionally
|
Paint p = AndroidGraphicFactory.INSTANCE.createPaint();
|
||||||
scale_factor_h = 1;
|
p.setColor(R.color.hrv_status_char_line_color);
|
||||||
} else {
|
Polyline polyline = new Polyline(p, AndroidGraphicFactory.INSTANCE);
|
||||||
scale_factor_w = 1;
|
polyline.setPoints(latlongs);
|
||||||
}
|
mapView.addLayer(polyline);
|
||||||
|
|
||||||
|
mapView.setCenter(new LatLong(minLat + (maxLat - minLat) / 2, minLon + (maxLon - minLon) / 2));
|
||||||
|
mapView.setZoomLevel((byte) 12);
|
||||||
|
|
||||||
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
mapView.setOnTouchListener((a, b) -> {
|
||||||
paint.setStrokeWidth(1);
|
final Intent startIntent = new Intent(requireContext(), MapsTrackActivity.class);
|
||||||
paint.setColor(getResources().getColor(R.color.chart_activity_light));
|
startIntent.putParcelableArrayListExtra("points", trackPoints);
|
||||||
|
requireContext().startActivity(startIntent);
|
||||||
for (GPSCoordinate p : trackPoints) {
|
return true;
|
||||||
float lat = (float) ((p.getLatitude() - minLat) / (maxLat - minLat));
|
});
|
||||||
float lon = (float) ((p.getLongitude() - minLon) / (maxLon - minLon));
|
mapView.setOnClickListener(v -> {
|
||||||
float alt = (float) ((p.getAltitude() - minAlt) / (maxAlt - minAlt));
|
final Intent startIntent = new Intent(requireContext(), MapsTrackActivity.class);
|
||||||
paint.setStrokeWidth(1 + alt); //make thicker with higher altitude, we could do more here
|
startIntent.putParcelableArrayListExtra("points", trackPoints);
|
||||||
canvas.drawPoint(CANVAS_SIZE * lon * scale_factor_w, CANVAS_SIZE * lat * scale_factor_h, paint);
|
requireContext().startActivity(startIntent);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Canvas createCanvas(ImageView imageView) {
|
private Canvas createCanvas(ImageView imageView) {
|
||||||
@@ -178,6 +187,4 @@ public class ActivitySummariesGpsFragment extends AbstractGBFragment {
|
|||||||
protected CharSequence getTitle() {
|
protected CharSequence getTitle() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
@@ -65,6 +65,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity;
|
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryPairingPreferenceActivity;
|
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryPairingPreferenceActivity;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsSettingsActivity;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
|
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver;
|
import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||||
@@ -372,6 +373,15 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pref = findPreference("pref_category_maps");
|
||||||
|
if (pref != null) {
|
||||||
|
pref.setOnPreferenceClickListener(preference -> {
|
||||||
|
Intent enableIntent = new Intent(requireContext(), MapsSettingsActivity.class);
|
||||||
|
startActivity(enableIntent);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pref = findPreference("pref_category_sleepasandroid");
|
pref = findPreference("pref_category_sleepasandroid");
|
||||||
if (pref != null) {
|
if (pref != null) {
|
||||||
pref.setOnPreferenceClickListener(preference -> {
|
pref.setOnPreferenceClickListener(preference -> {
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/* Copyright (C) 2024 José Rebelo
|
||||||
|
|
||||||
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Gadgetbridge is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.activities.maps;
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivityV2;
|
||||||
|
|
||||||
|
public class MapsSettingsActivity extends AbstractSettingsActivityV2 {
|
||||||
|
@Override
|
||||||
|
protected String fragmentTag() {
|
||||||
|
return MapsSettingsFragment.FRAGMENT_TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PreferenceFragmentCompat newFragment() {
|
||||||
|
return new MapsSettingsFragment();
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
/* Copyright (C) 2024 José Rebelo
|
||||||
|
|
||||||
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Gadgetbridge is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.activities.maps;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.maps.MapsManager;
|
||||||
|
|
||||||
|
public class MapsSettingsFragment extends AbstractPreferenceFragment {
|
||||||
|
static final String FRAGMENT_TAG = "MAP_SETTINGS_FRAGMENT";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(@Nullable final Bundle savedInstanceState, @Nullable final String rootKey) {
|
||||||
|
setPreferencesFromResource(R.xml.map_settings, rootKey);
|
||||||
|
|
||||||
|
final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
|
||||||
|
if (prefs == null) {
|
||||||
|
requireActivity().finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Preference prefDownload = Objects.requireNonNull(findPreference("maps_download"));
|
||||||
|
prefDownload.setOnPreferenceClickListener(preference -> {
|
||||||
|
startActivity(new Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
Uri.parse("https://ftp-stud.hs-esslingen.de/pub/Mirrors/download.mapsforge.org/maps/v5/")
|
||||||
|
));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final Preference prefFolder = Objects.requireNonNull(findPreference(MapsManager.PREF_MAPS_FOLDER));
|
||||||
|
final ActivityResultLauncher<Uri> mapsFolderChooser = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.OpenDocumentTree(),
|
||||||
|
localUri -> {
|
||||||
|
LOG.info("Maps folder: {}", localUri);
|
||||||
|
if (localUri != null) {
|
||||||
|
requireContext().getContentResolver().takePersistableUriPermission(localUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
prefs.edit()
|
||||||
|
.putString(MapsManager.PREF_MAPS_FOLDER, localUri.toString())
|
||||||
|
.apply();
|
||||||
|
prefFolder.setSummary(localUri.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
final String currentFolder = prefs.getString(MapsManager.PREF_MAPS_FOLDER, "");
|
||||||
|
prefFolder.setSummary(currentFolder);
|
||||||
|
prefFolder.setOnPreferenceClickListener(preference -> {
|
||||||
|
mapsFolderChooser.launch(null);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
/* Copyright (C) 2024 José Rebelo
|
||||||
|
|
||||||
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Gadgetbridge is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.activities.maps;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.mapsforge.core.graphics.Paint;
|
||||||
|
import org.mapsforge.core.model.LatLong;
|
||||||
|
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
|
||||||
|
import org.mapsforge.map.android.view.MapView;
|
||||||
|
import org.mapsforge.map.layer.overlay.Polyline;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.maps.MapsManager;
|
||||||
|
|
||||||
|
public class MapsTrackActivity extends AbstractGBActivity {
|
||||||
|
private MapView mapView;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_maps_track);
|
||||||
|
|
||||||
|
mapView = findViewById(R.id.activitygpsview);
|
||||||
|
MapsManager mapsManager = new MapsManager(this);
|
||||||
|
mapsManager.loadMaps(mapView);
|
||||||
|
|
||||||
|
final ArrayList<? extends GPSCoordinate> trackPoints = getIntent().getParcelableArrayListExtra("points");
|
||||||
|
|
||||||
|
double maxLat = (Collections.max(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude();
|
||||||
|
double minLat = (Collections.min(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude();
|
||||||
|
double maxLon = (Collections.max(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude();
|
||||||
|
double minLon = (Collections.min(trackPoints, new GPSCoordinate.compareLongitude())).getLongitude();
|
||||||
|
List<LatLong> latlongs = trackPoints.stream()
|
||||||
|
.map(p -> new LatLong(p.getLatitude(), p.getLongitude()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
Paint p = AndroidGraphicFactory.INSTANCE.createPaint();
|
||||||
|
p.setColor(R.color.hrv_status_char_line_color);
|
||||||
|
Polyline polyline = new Polyline(p, AndroidGraphicFactory.INSTANCE);
|
||||||
|
polyline.setPoints(latlongs);
|
||||||
|
mapView.addLayer(polyline);
|
||||||
|
|
||||||
|
mapView.setCenter(new LatLong(minLat + (maxLat - minLat) / 2, minLon + (maxLon - minLon) / 2));
|
||||||
|
mapView.setZoomLevel((byte) 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
|
final int itemId = item.getItemId();
|
||||||
|
if (itemId == android.R.id.home) {
|
||||||
|
// back button
|
||||||
|
finish();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSaveInstanceState(@NonNull final Bundle state) {
|
||||||
|
super.onSaveInstanceState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,10 @@
|
|||||||
package nodomain.freeyourgadget.gadgetbridge.model;
|
package nodomain.freeyourgadget.gadgetbridge.model;
|
||||||
|
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
@@ -24,7 +28,7 @@ import java.math.BigDecimal;
|
|||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
|
||||||
public class GPSCoordinate {
|
public class GPSCoordinate implements Parcelable {
|
||||||
private final double latitude;
|
private final double latitude;
|
||||||
private final double longitude;
|
private final double longitude;
|
||||||
private final double altitude;
|
private final double altitude;
|
||||||
@@ -50,6 +54,12 @@ public class GPSCoordinate {
|
|||||||
this(longitude, latitude, UNKNOWN_ALTITUDE);
|
this(longitude, latitude, UNKNOWN_ALTITUDE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected GPSCoordinate(Parcel in) {
|
||||||
|
latitude = in.readDouble();
|
||||||
|
longitude = in.readDouble();
|
||||||
|
altitude = in.readDouble();
|
||||||
|
}
|
||||||
|
|
||||||
public double getLatitude() {
|
public double getLatitude() {
|
||||||
return latitude;
|
return latitude;
|
||||||
}
|
}
|
||||||
@@ -156,6 +166,30 @@ public class GPSCoordinate {
|
|||||||
return "lon: " + formatLocation(longitude) + ", lat: " + formatLocation(latitude) + ", alt: " + formatLocation(altitude) + "m";
|
return "lon: " + formatLocation(longitude) + ", lat: " + formatLocation(latitude) + ", alt: " + formatLocation(altitude) + "m";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull final Parcel dest, final int flags) {
|
||||||
|
dest.writeDouble(latitude);
|
||||||
|
dest.writeDouble(longitude);
|
||||||
|
dest.writeDouble(altitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<GPSCoordinate> CREATOR = new Creator<GPSCoordinate>() {
|
||||||
|
@Override
|
||||||
|
public GPSCoordinate createFromParcel(Parcel in) {
|
||||||
|
return new GPSCoordinate(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GPSCoordinate[] newArray(int size) {
|
||||||
|
return new GPSCoordinate[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public static class compareLatitude implements Comparator<GPSCoordinate> {
|
public static class compareLatitude implements Comparator<GPSCoordinate> {
|
||||||
@Override
|
@Override
|
||||||
public int compare(GPSCoordinate trkPt1, GPSCoordinate trkPt2) {
|
public int compare(GPSCoordinate trkPt1, GPSCoordinate trkPt2) {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.util.maps;
|
||||||
|
|
||||||
|
import org.mapsforge.map.rendertheme.XmlRenderTheme;
|
||||||
|
import org.mapsforge.map.rendertheme.XmlRenderThemeMenuCallback;
|
||||||
|
import org.mapsforge.map.rendertheme.XmlThemeResourceProvider;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public enum MapTheme implements XmlRenderTheme {
|
||||||
|
DEFAULT("/assets/mapsforge/default.xml"),
|
||||||
|
OSMARENDER("/assets/mapsforge/osmarender.xml"),
|
||||||
|
;
|
||||||
|
|
||||||
|
private XmlRenderThemeMenuCallback menuCallback;
|
||||||
|
private final String path;
|
||||||
|
|
||||||
|
MapTheme(String path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public XmlRenderThemeMenuCallback getMenuCallback() {
|
||||||
|
return menuCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRelativePathPrefix() {
|
||||||
|
return "/assets/";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getRenderThemeAsStream() {
|
||||||
|
return getClass().getResourceAsStream(this.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public XmlThemeResourceProvider getResourceProvider() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMenuCallback(final XmlRenderThemeMenuCallback menuCallback) {
|
||||||
|
this.menuCallback = menuCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setResourceProvider(final XmlThemeResourceProvider resourceProvider) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/* Copyright (C) 2024 José Rebelo
|
||||||
|
|
||||||
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Gadgetbridge is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.util.maps;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
|
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
|
||||||
|
import org.mapsforge.map.android.util.AndroidUtil;
|
||||||
|
import org.mapsforge.map.android.view.MapView;
|
||||||
|
import org.mapsforge.map.datastore.MultiMapDataStore;
|
||||||
|
import org.mapsforge.map.layer.cache.TileCache;
|
||||||
|
import org.mapsforge.map.layer.renderer.TileRendererLayer;
|
||||||
|
import org.mapsforge.map.reader.MapFile;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||||
|
|
||||||
|
public final class MapsManager {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(MapsManager.class);
|
||||||
|
|
||||||
|
public static final String PREF_MAPS_FOLDER = "maps_folder";
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
|
public MapsManager(final Context context) {
|
||||||
|
this.mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadMaps(final MapView mapView) {
|
||||||
|
AndroidGraphicFactory.createInstance(GBApplication.app());
|
||||||
|
GBPrefs prefs = GBApplication.getPrefs();
|
||||||
|
|
||||||
|
String folderUri = prefs.getString(PREF_MAPS_FOLDER, "");
|
||||||
|
if (folderUri.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DocumentFile folder = DocumentFile.fromTreeUri(mContext, Uri.parse(folderUri));
|
||||||
|
if (folder == null || folder.listFiles().length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiMapDataStore multiMapDataStore = new MultiMapDataStore(MultiMapDataStore.DataPolicy.RETURN_ALL);
|
||||||
|
|
||||||
|
final DocumentFile[] documentFiles = folder.listFiles();
|
||||||
|
|
||||||
|
LOG.debug("Got {} map files", documentFiles.length);
|
||||||
|
|
||||||
|
for (final DocumentFile documentFile : documentFiles) {
|
||||||
|
if (!documentFile.canRead()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assert documentFile.getName() != null;
|
||||||
|
if (!documentFile.getName().endsWith(".map")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.debug("Loading {}", documentFile.getName());
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileInputStream inputStream = (FileInputStream) mContext.getContentResolver().openInputStream(documentFile.getUri());
|
||||||
|
assert inputStream != null;
|
||||||
|
MapFile mapFile = new MapFile(inputStream, 0, null);
|
||||||
|
multiMapDataStore.addMapDataStore(mapFile, true, true);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TileCache tileCache = AndroidUtil.createTileCache(mContext, "mapcache",
|
||||||
|
mapView.getModel().displayModel.getTileSize(), 1f,
|
||||||
|
mapView.getModel().frameBufferModel.getOverdrawFactor());
|
||||||
|
|
||||||
|
TileRendererLayer tileRendererLayer = new TileRendererLayer(tileCache, multiMapDataStore,
|
||||||
|
mapView.getModel().mapViewPosition, AndroidGraphicFactory.INSTANCE);
|
||||||
|
tileRendererLayer.setXmlRenderTheme(MapTheme.DEFAULT);
|
||||||
|
|
||||||
|
mapView.getLayerManager().getLayers().add(tileRendererLayer);
|
||||||
|
|
||||||
|
// TODO on exit mapView.destroyAll();
|
||||||
|
//AndroidGraphicFactory.clearResourceMemoryCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="#7E7E7E"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z" />
|
||||||
|
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.maps.MapsTrackActivity">
|
||||||
|
|
||||||
|
<org.mapsforge.map.android.view.MapView
|
||||||
|
android:id="@+id/activitygpsview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
</RelativeLayout>
|
||||||
@@ -18,11 +18,33 @@
|
|||||||
android:text="@string/gps_track"
|
android:text="@string/gps_track"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<ImageView
|
<LinearLayout
|
||||||
android:id="@+id/activitygpsview"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:src="@tools:sample/avatars" />
|
android:layout_marginBottom="20dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/gpsWarning"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="Maps not found" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<org.mapsforge.map.android.view.MapView
|
||||||
|
android:id="@+id/activitygpsview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|||||||
@@ -3786,7 +3786,6 @@
|
|||||||
<string name="ultrahuman_airplane_mode_question">Activate airplane mode?</string>
|
<string name="ultrahuman_airplane_mode_question">Activate airplane mode?</string>
|
||||||
<string name="ultrahuman_airplane_mode_title">airplane mode</string>
|
<string name="ultrahuman_airplane_mode_title">airplane mode</string>
|
||||||
<string name="ultrahuman_unhandled_error_response">received unhandled response code 0x%x for operation 0x%x</string>
|
<string name="ultrahuman_unhandled_error_response">received unhandled response code 0x%x for operation 0x%x</string>
|
||||||
|
|
||||||
<string name="huawei_stress_test_enable">Automatic stress test</string>
|
<string name="huawei_stress_test_enable">Automatic stress test</string>
|
||||||
<string name="huawei_stress_test_enable_summary">Enable or disable automatic stress test</string>
|
<string name="huawei_stress_test_enable_summary">Enable or disable automatic stress test</string>
|
||||||
<string name="huawei_stress_no_calibration_data">No initial data - calibrate first</string>
|
<string name="huawei_stress_no_calibration_data">No initial data - calibrate first</string>
|
||||||
@@ -3801,10 +3800,12 @@
|
|||||||
<string name="huawei_stress_calibrate_finish">Finish</string>
|
<string name="huawei_stress_calibrate_finish">Finish</string>
|
||||||
<string name="huawei_stress_calibrate_again">Calibrate again</string>
|
<string name="huawei_stress_calibrate_again">Calibrate again</string>
|
||||||
<string name="huawei_stress_calibrate_adjust_score">Adjust score</string>
|
<string name="huawei_stress_calibrate_adjust_score">Adjust score</string>
|
||||||
|
|
||||||
<string name="banglejs_notification_missed_call_title">Missed call</string>
|
<string name="banglejs_notification_missed_call_title">Missed call</string>
|
||||||
<string name="banglejs_notification_missed_call_source">Incoming call</string>
|
<string name="banglejs_notification_missed_call_source">Incoming call</string>
|
||||||
<string name="banglejs_notification_missed_call_suppressed_caller_id">Unknown caller. ID is suppressed</string>
|
<string name="banglejs_notification_missed_call_suppressed_caller_id">Unknown caller. ID is suppressed</string>
|
||||||
|
|
||||||
<string name="huawei_voice_boost">Voice Boost</string>
|
<string name="huawei_voice_boost">Voice Boost</string>
|
||||||
|
<string name="maps">Maps</string>
|
||||||
|
<string name="maps_settings">Maps settings</string>
|
||||||
|
<string name="maps_download_title">Download Maps</string>
|
||||||
|
<string name="maps_download_summary">Click here to download map files. They should be saved to the folder selected in the preference below.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<Preference
|
||||||
|
android:icon="@drawable/ic_download"
|
||||||
|
android:key="maps_download"
|
||||||
|
android:persistent="false"
|
||||||
|
android:summary="@string/maps_download_summary"
|
||||||
|
android:title="@string/maps_download_title" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:icon="@drawable/ic_folder"
|
||||||
|
android:key="maps_folder"
|
||||||
|
android:title="@string/folder" />
|
||||||
|
</PreferenceScreen>
|
||||||
@@ -326,6 +326,11 @@
|
|||||||
android:summary="@string/pref_summary_opentracks_packagename"
|
android:summary="@string/pref_summary_opentracks_packagename"
|
||||||
android:title="@string/pref_title_opentracks_packagename" />
|
android:title="@string/pref_title_opentracks_packagename" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="pref_category_maps"
|
||||||
|
android:title="@string/maps"
|
||||||
|
app:iconSpaceReserved="false" />
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/ic_activity_sleep"
|
android:icon="@drawable/ic_activity_sleep"
|
||||||
android:key="pref_category_sleepasandroid"
|
android:key="pref_category_sleepasandroid"
|
||||||
|
|||||||
@@ -28,6 +28,12 @@
|
|||||||
"com.github.wax911.android-emojify:*"
|
"com.github.wax911.android-emojify:*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"groupName": "mapsforge",
|
||||||
|
"matchPackagePrefixes": [
|
||||||
|
"com.github.mapsforge.mapsforge"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "solarpositioning v2 needs java 17, we need to stay on v0",
|
"description": "solarpositioning v2 needs java 17, we need to stay on v0",
|
||||||
"allowedVersions": "<2.0",
|
"allowedVersions": "<2.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user