diff --git a/app/build.gradle b/app/build.gradle
index 138546b4..df2f007e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -102,6 +102,8 @@ android {
buildConfigField 'String', 'WS_API_USER_ID', "\"$wsApiUserId\""
buildConfigField 'String', 'WS_API_KEY', "\"$wsApiKey\""
+ multiDexEnabled true
+
}
flavorDimensions "mode"
productFlavors {
@@ -164,23 +166,26 @@ ext {
androidxVersion = '1.1.0'
androidxCardViewVersion = '1.0.0'
androidxConstraintLayoutVersion = '1.1.3'
- androidxTestVersion = '1.0.0'
- assertjVersion = '3.13.2'
- autoServiceVersion = '1.0-rc4'
+ androidxTestVersion = '1.2.0'
+ assertjVersion = '3.14.0'
+ autoServiceVersion = '1.0-rc6'
bubbleSeekbarVersion = '3.20'
butterknifeVersion = '10.2.0'
- daggerVersion = '2.24'
- glideVersion = '4.9.0'
+ daggerVersion = '2.25.2'
+ glideVersion = '4.10.0'
gsonVersion = '2.8.6'
jsonVersion = '20190722'
junitVersion = '4.12'
+ multiDexVersion = '2.0.1'
mockitoVersion = '2.28.2'
- okHttpVersion = '3.12.0'
+ okHttpVersion = '4.2.2'
osmbonuspackVersion = '6.6.0'
- osmdroidVersion = '6.1.0'
+ osmdroidMapforgeVersion = '6.1.2'
+ osmdroidVersion = '6.1.2'
playServicesVersion = '17.0.0'
- retrofitVersion = '2.6.1' // Dictates version of okhttp in their dependencies which clashes in tests.
- robolectricVersion = '4.3'
+ retrofitVersion = '2.6.2'
+ // Dictates version of okhttp in their dependencies which clashes in tests.
+ robolectricVersion = '4.3.1'
rxAndroidVersion = '2.1.1'
rxJavaVersion = '2.2.12'
securekeysVersion = '2.2.0' // Adjust the version in the project's build.gradle, too.
@@ -196,6 +201,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:$androidxVersion"
implementation "androidx.cardview:cardview:$androidxCardViewVersion"
implementation "androidx.constraintlayout:constraintlayout:$androidxConstraintLayoutVersion"
+ implementation "androidx.multidex:multidex:$multiDexVersion"
implementation "androidx.preference:preference:$androidxVersion"
implementation "com.github.bumptech.glide:glide:$glideVersion"
implementation "com.github.MKergall:osmbonuspack:$osmbonuspackVersion"
@@ -213,6 +219,7 @@ dependencies {
implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
implementation "org.osmdroid:osmdroid-android:$osmdroidVersion"
+ implementation "org.osmdroid:osmdroid-mapsforge:$osmdroidMapforgeVersion"
googleImplementation "com.google.android.gms:play-services-analytics:$playServicesVersion"
diff --git a/app/src/google/res/xml/preferences.xml b/app/src/google/res/xml/preferences.xml
index f3eabd3e..e5482a74 100644
--- a/app/src/google/res/xml/preferences.xml
+++ b/app/src/google/res/xml/preferences.xml
@@ -1,37 +1,74 @@
+
-
-
-
-
-
-
-
-
-
-
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bcf96846..cb8002d0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -13,7 +13,7 @@
-
+
diff --git a/app/src/main/java/fi/bitrite/android/ws/BaseWSAndroidApplication.java b/app/src/main/java/fi/bitrite/android/ws/BaseWSAndroidApplication.java
index a5f0096b..61d5589e 100644
--- a/app/src/main/java/fi/bitrite/android/ws/BaseWSAndroidApplication.java
+++ b/app/src/main/java/fi/bitrite/android/ws/BaseWSAndroidApplication.java
@@ -1,6 +1,5 @@
package fi.bitrite.android.ws;
-import android.app.Application;
import android.content.SharedPreferences;
import com.bumptech.glide.request.RequestOptions;
@@ -8,6 +7,7 @@
import javax.inject.Inject;
+import androidx.multidex.MultiDexApplication;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.HasAndroidInjector;
@@ -16,7 +16,7 @@
import fi.bitrite.android.ws.repository.SettingsRepository;
import fi.bitrite.android.ws.util.WSGlide;
-public abstract class BaseWSAndroidApplication extends Application implements HasAndroidInjector {
+public abstract class BaseWSAndroidApplication extends MultiDexApplication implements HasAndroidInjector {
public static final String TAG = "WSAndroidApplication";
private static AppInjector mAppInjector;
diff --git a/app/src/main/java/fi/bitrite/android/ws/auth/Authenticator.java b/app/src/main/java/fi/bitrite/android/ws/auth/Authenticator.java
index 2bb6a4e5..1effb618 100644
--- a/app/src/main/java/fi/bitrite/android/ws/auth/Authenticator.java
+++ b/app/src/main/java/fi/bitrite/android/ws/auth/Authenticator.java
@@ -218,7 +218,7 @@ public enum ErrorCause {
WrongAPIKey,
AccountTemporarilyBlocked,
IpTemporarilyBlocked,
- Unknown;
+ Unknown
}
public final ErrorCause errorCause;
diff --git a/app/src/main/java/fi/bitrite/android/ws/model/MapsForgeTheme.java b/app/src/main/java/fi/bitrite/android/ws/model/MapsForgeTheme.java
new file mode 100644
index 00000000..2a399eb8
--- /dev/null
+++ b/app/src/main/java/fi/bitrite/android/ws/model/MapsForgeTheme.java
@@ -0,0 +1,46 @@
+package fi.bitrite.android.ws.model;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+public class MapsForgeTheme implements Serializable {
+ private String name;
+ private String id;
+ private Map localizedNames;
+ private String filePath;
+
+ public MapsForgeTheme(String name, String id, String filePath) {
+ this.name = name;
+ this.id = id;
+ this.filePath = filePath;
+ localizedNames = new HashMap<>();
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getFilePath() {
+ return filePath;
+ }
+
+ public String getLocalizedDisplayName(String lang) {
+ if (!localizedNames.containsKey(lang)) {
+ lang = "en";
+ }
+ if (localizedNames.get(lang) != null) {
+ return localizedNames.get(lang) + " (" + name + ")";
+ } else {
+ return name;
+ }
+ }
+
+ public void addLocalizedName(String lang, String name) {
+ localizedNames.put(lang, name);
+ }
+}
diff --git a/app/src/main/java/fi/bitrite/android/ws/repository/BaseSettingsRepository.java b/app/src/main/java/fi/bitrite/android/ws/repository/BaseSettingsRepository.java
index 2b4ee34a..4de4488c 100644
--- a/app/src/main/java/fi/bitrite/android/ws/repository/BaseSettingsRepository.java
+++ b/app/src/main/java/fi/bitrite/android/ws/repository/BaseSettingsRepository.java
@@ -3,13 +3,23 @@
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
-import android.preference.PreferenceManager;
import android.text.TextUtils;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
import androidx.annotation.VisibleForTesting;
+import androidx.preference.PreferenceManager;
import fi.bitrite.android.ws.R;
+import fi.bitrite.android.ws.model.MapsForgeTheme;
import fi.bitrite.android.ws.model.ZoomedLocation;
public abstract class BaseSettingsRepository {
@@ -30,13 +40,21 @@ public enum DistanceUnit {
private final static String KEYPREFIX_USERFILTER_VALUE = "userfilter-value-";
private final String mKeyDistanceUnit;
- private final String mKeyTileSource;
+
+ private final String mOnlineMapSource;
+
+ private final String mOfflineMapEnabled;
+ private final String mOfflineMapSelection;
+ private final String mOfflineMapSourceFiles;
+ private final String mOfflineThemeSourceFiles;
+ private final String mOfflineThemeSelection;
+
private final String mKeyMessageRefreshInterval;
private final String mKeyDataSaverMode;
private final String mKeyDevSimulateNoNetwork;
private final String mDefaultDistanceUnit;
- private final String mDefaultTileSource = TileSourceFactory.OpenTopo.name();
+ private final String mDefaultOnlineMapSource = TileSourceFactory.OpenTopo.name();
private final int mDefaultMessageRefreshInterval;
private final boolean mDefaultDataSaverMode;
private final boolean mDefaultDevSimulateNoNetwork;
@@ -52,12 +70,22 @@ public enum DistanceUnit {
private final String mDistanceUnitKilometerLong;
private final String mDistanceUnitMilesLong;
+ private final Gson gson;
+
BaseSettingsRepository(Context context) {
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
final Resources res = context.getResources();
mKeyDistanceUnit = res.getString(R.string.prefs_distance_unit_key);
- mKeyTileSource = res.getString(R.string.prefs_tile_source_key);
+
+ mOnlineMapSource = res.getString(R.string.prefs_online_map_source);
+
+ mOfflineMapEnabled= res.getString(R.string.prefs_offline_map_enabled);
+ mOfflineMapSelection = res.getString(R.string.prefs_offline_map_selection);
+ mOfflineMapSourceFiles = res.getString(R.string.prefs_offline_map_source_files);
+ mOfflineThemeSelection = res.getString(R.string.prefs_offline_theme_selection);
+ mOfflineThemeSourceFiles = res.getString(R.string.prefs_offline_theme_source_files);
+
mKeyMessageRefreshInterval = res.getString(R.string.prefs_message_refresh_interval_min_key);
mKeyDataSaverMode = res.getString(R.string.prefs_data_saver_mode_key);
mKeyDevSimulateNoNetwork = res.getString(R.string.prefs_dev_simulate_no_network_key);
@@ -80,44 +108,41 @@ public enum DistanceUnit {
mDistanceUnitMilesShort = res.getString(R.string.distance_unit_miles_short);
mDistanceUnitKilometerLong = res.getString(R.string.distance_unit_kilometers_long);
mDistanceUnitMilesLong = res.getString(R.string.distance_unit_miles_long);
+
+ gson = new Gson();
}
public DistanceUnit getDistanceUnit() {
String distanceUnitStr =
mSharedPreferences.getString(mKeyDistanceUnit, mDefaultDistanceUnit);
- if (TextUtils.equals(distanceUnitStr, mDistanceUnitKilometerRaw)) {
- return DistanceUnit.KILOMETER;
- } else if (TextUtils.equals(distanceUnitStr, mDistanceUnitMilesRaw)) {
+ if (TextUtils.equals(distanceUnitStr, mDistanceUnitMilesRaw)) {
return DistanceUnit.MILES;
- } else {
- assert false;
- return null;
}
+ return DistanceUnit.KILOMETER;
}
+
public String getDistanceUnitLong() {
DistanceUnit distanceUnit = getDistanceUnit();
switch (distanceUnit) {
- case KILOMETER: return mDistanceUnitKilometerLong;
case MILES: return mDistanceUnitMilesLong;
+ case KILOMETER:
default:
- assert false;
- return null;
+ return mDistanceUnitKilometerLong;
}
}
public String getDistanceUnitShort() {
DistanceUnit distanceUnit = getDistanceUnit();
switch (distanceUnit) {
- case KILOMETER: return mDistanceUnitKilometerShort;
case MILES: return mDistanceUnitMilesShort;
+ case KILOMETER:
default:
- assert false;
- return null;
+ return mDistanceUnitKilometerShort;
}
}
- public String getTileSourceStr() {
- return mSharedPreferences.getString(mKeyTileSource, mDefaultTileSource);
+ public String getOnlineMapSourceStr() {
+ return mSharedPreferences.getString(mOnlineMapSource, mDefaultOnlineMapSource);
}
private void setLocation(String key, ZoomedLocation position) {
@@ -130,6 +155,59 @@ private void setLocation(String key, ZoomedLocation position) {
}
+ public boolean isOfflineMapEnabled() {
+ return mSharedPreferences.getBoolean(mOfflineMapEnabled, false);
+ }
+
+ public void setAvailableOfflineMapSources(Set sources) {
+ mSharedPreferences.edit().putString(mOfflineMapSourceFiles, gson.toJson(sources)).apply();
+ }
+
+ public Set getAvailableOfflineMapSources() {
+ return new HashSet<>(
+ gson.fromJson(mSharedPreferences.getString(mOfflineMapSourceFiles, ""),
+ new TypeToken>() {}.getType()));
+ }
+
+ public Set getAvailableOfflineMapSelection() {
+ return mSharedPreferences.getStringSet(mOfflineMapSelection, new HashSet<>());
+ }
+
+ public File[] getOfflineMapSourceFiles() {
+ Set sources = getAvailableOfflineMapSources();
+ Set sourceFiles = new HashSet<>();
+ for (String source : sources) {
+ File f = new File(source);
+ if (f.exists()) {
+ sourceFiles.add(new File(source));
+ }
+ }
+ return sourceFiles.toArray(new File[0]);
+ }
+
+ public void setAvailableOfflineThemeSources(List sources) {
+ mSharedPreferences.edit().putString(mOfflineThemeSourceFiles, gson.toJson(sources)).apply();
+ }
+
+ public List getAvailableOfflineThemeSources() {
+ List themes = gson.fromJson(mSharedPreferences.getString(mOfflineThemeSourceFiles, ""),
+ new TypeToken>(){}.getType());
+ if (themes == null) {
+ themes = new ArrayList<>();
+ }
+ themes.add(new MapsForgeTheme("Default theme", "default_theme", ""));
+ return themes;
+ }
+
+ public void setSelectedOfflineMapTheme(MapsForgeTheme theme) {
+ mSharedPreferences.edit().putString(mOfflineThemeSelection, gson.toJson(theme)).apply();
+ }
+
+ public MapsForgeTheme getSelectedOfflineMapTheme() {
+ return gson.fromJson(mSharedPreferences.getString(mOfflineThemeSelection, ""),
+ new TypeToken(){}.getType());
+ }
+
public ZoomedLocation getLastMapLocation(boolean defaultIfNone) {
if (!mSharedPreferences.contains(KEY_MAP_LAST_LOCATION + KEYSUFFIX_LOCATION_LATITUDE)
&& !defaultIfNone) {
@@ -222,4 +300,5 @@ public String getDataSaverModeKey() {
public String getDevSimulateNoNetworkKey() {
return mKeyDevSimulateNoNetwork;
}
+
}
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/MapFragment.java b/app/src/main/java/fi/bitrite/android/ws/ui/MapFragment.java
index ccac7b05..35b771ec 100644
--- a/app/src/main/java/fi/bitrite/android/ws/ui/MapFragment.java
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/MapFragment.java
@@ -24,6 +24,8 @@
import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import org.mapsforge.map.rendertheme.ExternalRenderTheme;
+import org.mapsforge.map.rendertheme.XmlRenderTheme;
import org.osmdroid.api.IGeoPoint;
import org.osmdroid.api.IMapController;
import org.osmdroid.bonuspack.clustering.StaticCluster;
@@ -31,9 +33,13 @@
import org.osmdroid.events.MapListener;
import org.osmdroid.events.ScrollEvent;
import org.osmdroid.events.ZoomEvent;
+import org.osmdroid.mapsforge.MapsForgeTileProvider;
+import org.osmdroid.mapsforge.MapsForgeTileSource;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
+import org.osmdroid.tileprovider.util.SimpleRegisterReceiver;
import org.osmdroid.util.BoundingBox;
import org.osmdroid.util.GeoPoint;
+import org.osmdroid.views.CustomZoomButtonsController;
import org.osmdroid.views.MapView;
import org.osmdroid.views.overlay.Marker;
import org.osmdroid.views.overlay.ScaleBarOverlay;
@@ -41,6 +47,8 @@
import org.osmdroid.views.overlay.mylocation.IMyLocationProvider;
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay;
+import java.io.File;
+import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -63,6 +71,7 @@
import butterknife.Unbinder;
import fi.bitrite.android.ws.R;
import fi.bitrite.android.ws.api.response.UserSearchByLocationResponse;
+import fi.bitrite.android.ws.model.MapsForgeTheme;
import fi.bitrite.android.ws.model.SimpleUser;
import fi.bitrite.android.ws.model.User;
import fi.bitrite.android.ws.model.ZoomedLocation;
@@ -70,6 +79,7 @@
import fi.bitrite.android.ws.repository.FavoriteRepository;
import fi.bitrite.android.ws.repository.Resource;
import fi.bitrite.android.ws.repository.SettingsRepository;
+import fi.bitrite.android.ws.ui.util.OfflineMapHelper;
import fi.bitrite.android.ws.ui.util.UserFilterManager;
import fi.bitrite.android.ws.ui.util.UserMarker;
import fi.bitrite.android.ws.ui.util.UserMarkerClusterer;
@@ -174,6 +184,10 @@ public void onCreate(Bundle savedInstanceState) {
mMarkerClusterer.setOnClusterClickListener(this::onClusterClick);
mHideLocationBtn = mSettingsRepository.getHideLocationButton();
+
+ if (mSettingsRepository.isOfflineMapEnabled()) {
+ MapsForgeTileSource.createInstance(requireActivity().getApplication());
+ }
}
@Nullable
@@ -182,7 +196,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
@Nullable Bundle savedInstanceState) {
Context context = getContext();
Configuration.getInstance().load(
- context, PreferenceManager.getDefaultSharedPreferences(context));
+ context, PreferenceManager.getDefaultSharedPreferences(requireContext()));
// setting this before the layout is inflated is a good idea
// it 'should' ensure that the map has a writable location for the map cache, even without
// permissions. if no tiles are displayed, you can try overriding the cache path using
@@ -209,14 +223,45 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
mLastPosition = null;
mMap.setVerticalMapRepetitionEnabled(false);
- mMap.setBuiltInZoomControls(false);
+ mMap.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);
mMap.setMultiTouchControls(true);
- String tileSourceStr = mSettingsRepository.getTileSourceStr();
- if (!TileSourceFactory.containsTileSource(tileSourceStr)) {
- tileSourceStr = TileSourceFactory.DEFAULT_TILE_SOURCE.name();
+ if (mSettingsRepository.isOfflineMapEnabled() &&
+ OfflineMapHelper.containsExistingFile(mSettingsRepository.getOfflineMapSourceFiles())) {
+ // use offline map
+ MapsForgeTheme mapsForgeTheme = mSettingsRepository.getSelectedOfflineMapTheme();
+ if (mapsForgeTheme == null) {
+ // create empty default theme
+ mapsForgeTheme = new MapsForgeTheme("Default", "default_theme_id", "");
+ }
+
+ File style = new File(mapsForgeTheme.getFilePath());
+ XmlRenderTheme theme = null;
+
+ try {
+ theme = new ExternalRenderTheme(style);
+ OfflineMapHelper.setThemeStyle(theme, mapsForgeTheme.getId());
+ } catch (FileNotFoundException ignored) {
+ // if theme == null, default rendering theme is used.
+ }
+
+ // TODO: set map language when osmdroid updated to v6.0.3
+ MapsForgeTileSource fromFiles = MapsForgeTileSource.createFromFiles(
+ mSettingsRepository.getOfflineMapSourceFiles(), theme, style.getName());
+
+ MapsForgeTileProvider forge = new MapsForgeTileProvider(
+ new SimpleRegisterReceiver(getContext()),
+ fromFiles, null
+ );
+ mMap.setTileProvider(forge);
+ } else {
+ // use online map
+ String tileSourceStr = mSettingsRepository.getOnlineMapSourceStr();
+ if (!TileSourceFactory.containsTileSource(tileSourceStr)) {
+ tileSourceStr = TileSourceFactory.DEFAULT_TILE_SOURCE.name();
+ }
+ mMap.setTileSource(TileSourceFactory.getTileSource(tileSourceStr));
}
- mMap.setTileSource(TileSourceFactory.getTileSource(tileSourceStr));
if (hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
addMyLocationOverlay();
@@ -332,33 +377,32 @@ private void askForPermission(String permission, int requestCode) {
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
- switch (requestCode) {
- case REQUEST_CODE_FINE_LOCATION:
- if (grantResults.length > 0
- && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- addMyLocationOverlay();
- startLocationManager();
-
- // Delays moving to the current location by a tiny bit as the location manager
- // was just started and the current location is therefore not yet known. Without
- // this delay the "location not known" toast was shown and just afterwards the
- // map was moved to the current position. However, if indeed the current
- // position is not yet known that toast is shown even after that delay.
- Disposable unused = Completable
- .timer(100, TimeUnit.MILLISECONDS)
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(this::onGotoCurrentLocationClicked);
- } else {
- // if the permission for location is denied with checked "Never ask again",
- // the current location button will be hidden.
- mHideLocationBtn = !shouldShowRequestPermissionRationale(permissions[0]);
- }
+ if (requestCode != REQUEST_CODE_FINE_LOCATION) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ return;
+ }
+
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ addMyLocationOverlay();
+ startLocationManager();
- setGotoCurrentLocationStatus();
- break;
- default:
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ // Delays moving to the current location by a tiny bit as the location manager
+ // was just started and the current location is therefore not yet known. Without
+ // this delay the "location not known" toast was shown and just afterwards the
+ // map was moved to the current position. However, if indeed the current
+ // position is not yet known that toast is shown even after that delay.
+ Disposable unused = Completable
+ .timer(100, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(this::onGotoCurrentLocationClicked);
+ } else {
+ // if the permission for location is denied with checked "Never ask again",
+ // the current location button will be hidden.
+ mHideLocationBtn = !shouldShowRequestPermissionRationale(permissions[0]);
}
+
+ setGotoCurrentLocationStatus();
}
private void startLocationManager() {
@@ -633,10 +677,7 @@ private boolean markerUpdateRequired(final Marker existingMarker, SimpleUser use
if (!existingMarker.getPosition().equals(user.location)) {
return true;
}
- if (!existingMarker.getIcon().equals(getMarkerIconForHost(isFavoriteHost))) {
- return true;
- }
- return false;
+ return !existingMarker.getIcon().equals(getMarkerIconForHost(isFavoriteHost));
}
private Drawable getMarkerIconForHost(boolean isFavoriteHost) {
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/SettingsFragment.java b/app/src/main/java/fi/bitrite/android/ws/ui/SettingsFragment.java
index 22ee3413..b0395b30 100644
--- a/app/src/main/java/fi/bitrite/android/ws/ui/SettingsFragment.java
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/SettingsFragment.java
@@ -1,33 +1,53 @@
package fi.bitrite.android.ws.ui;
+import android.Manifest;
+import android.content.Intent;
import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
+import android.net.Uri;
import android.os.Bundle;
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.Fragment;
-import androidx.preference.ListPreference;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceFragmentCompat;
+import android.text.Html;
import android.text.TextUtils;
+import android.util.Log;
+
+import com.google.gson.Gson;
import org.osmdroid.tileprovider.tilesource.ITileSource;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
+import java.io.File;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
+import java.util.Locale;
+import java.util.Set;
import javax.inject.Inject;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.preference.ListPreference;
+import androidx.preference.MultiSelectListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.SwitchPreference;
import butterknife.BindString;
import butterknife.ButterKnife;
import fi.bitrite.android.ws.BuildConfig;
import fi.bitrite.android.ws.R;
import fi.bitrite.android.ws.di.Injectable;
+import fi.bitrite.android.ws.model.MapsForgeTheme;
import fi.bitrite.android.ws.repository.SettingsRepository;
import fi.bitrite.android.ws.ui.preference.RefreshIntervalPreferenceDialogFragment;
import fi.bitrite.android.ws.ui.util.ActionBarTitleHelper;
+import fi.bitrite.android.ws.ui.util.OfflineMapHelper;
public class SettingsFragment extends PreferenceFragmentCompat implements Injectable,
SharedPreferences.OnSharedPreferenceChangeListener {
@@ -35,10 +55,21 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Inject
@Inject ActionBarTitleHelper mActionBarTitleHelper;
@Inject SettingsRepository mSettingsRepository;
+ @BindString(R.string.prefs_online_map_source) String mOnlineMapSource;
+
+ @BindString(R.string.prefs_offline_map_enabled) String mOfflineMapSwitch;
+ @BindString(R.string.prefs_offline_map_selection) String mOfflineMapSelection;
+ @BindString(R.string.prefs_offline_map_source_files) String mOfflineMapSourceFiles;
+ @BindString(R.string.prefs_offline_theme_selection) String mOfflineThemeSelection;
+ @BindString(R.string.prefs_offline_theme_source_files) String mOfflineThemeSourceFiles;
+
+ @BindString(R.string.prefs_offline_map_prefer_installed_locales) String mOfflineMapLanguage;
+
@BindString(R.string.prefs_distance_unit_key) String mKeyDistanceUnit;
- @BindString(R.string.prefs_tile_source_key) String mTileMapSource;
@BindString(R.string.prefs_message_refresh_interval_min_key) String mKeyMessageRefreshInterval;
+ private final static int MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 667;
+
public static Fragment create() {
Bundle bundle = new Bundle();
@@ -50,7 +81,7 @@ public static Fragment create() {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- ButterKnife.bind(this, getActivity());
+ ButterKnife.bind(this, requireActivity());
}
@Override
@@ -60,6 +91,20 @@ public void onResume() {
mActionBarTitleHelper.set(getString(R.string.title_fragment_settings));
mSettingsRepository.registerOnChangeListener(this);
+
+
+ findPreference(mOfflineMapSwitch).setSummaryProvider(
+ preference -> {
+ String fileDir = OfflineMapHelper.defaultMapDataDirectory(requireContext());
+ List fnames = new ArrayList<>();
+ for (File f : mSettingsRepository.getOfflineMapSourceFiles()) {
+ fnames.add(f.getAbsolutePath().replace(fileDir, ""));
+ }
+ return TextUtils.join(", ", fnames);
+ }
+ );
+
+ setOfflinemapSettings();
setSummary();
}
@@ -69,6 +114,11 @@ public void onPause() {
super.onPause();
}
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
// Load the preferences from an XML resource
@@ -81,25 +131,65 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (key == null) {
+ return;
+ }
+
+ if (key.equals(mOfflineMapSwitch) && sharedPreferences.getBoolean(mOfflineMapSwitch, false)) {
+ // ask for storage permission
+ if (ContextCompat.checkSelfPermission(requireActivity(),
+ Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ // Permission is not granted
+ requestPermissions(
+ new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
+ MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
+ } else {
+ searchOfflineMapData();
+ }
+ }
+ setOfflinemapSettings();
setSummary();
}
- private void setSummary() {
- // distance units
- findPreference(mKeyDistanceUnit).setSummary(getString(
- R.string.prefs_distance_unit_summary,
- mSettingsRepository.getDistanceUnitLong()));
- // map sources
- setAvailableMapSources((ListPreference) findPreference(mTileMapSource));
+ private void searchOfflineMapData() {
+ OfflineMapHelper.searchOfflineMapData(mSettingsRepository,false, requireActivity());
+ Set availableSources = mSettingsRepository.getAvailableOfflineMapSources();
+ if (availableSources.isEmpty()) {
+ // no maps found. show help and uncheck button
+ showMapDownloadDialog();
+ ((SwitchPreference) findPreference(mOfflineMapSwitch)).setChecked(false);
+ }
+ }
- // message refresh interval
- Resources res = getResources();
- int intervalMin = mSettingsRepository.getMessageRefreshIntervalMin();
- findPreference(mKeyMessageRefreshInterval).setSummary(intervalMin > 0
- ? res.getQuantityString(R.plurals.prefs_message_refresh_interval_min_summary,
- intervalMin, intervalMin)
- : getString(R.string.prefs_message_refresh_interval_min_summary_disabled));
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (requestCode != MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ return;
+ }
+
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ searchOfflineMapData();
+ } else {
+ ((SwitchPreference) findPreference(mOfflineMapSwitch)).setChecked(false);
+ }
+ setOfflinemapSettings();
+ }
+
+
+ private void showMapDownloadDialog() {
+ new AlertDialog.Builder(requireContext())
+ .setTitle(R.string.no_offline_maps_found_title)
+ .setMessage(Html.fromHtml(String.format(getString(R.string.no_offline_maps_found_message), OfflineMapHelper.defaultMapDataDirectory(requireContext()))))
+ .setPositiveButton(R.string.alert_neutral_button, (dialog, id) -> dialog.dismiss())
+ .setNeutralButton(R.string.open_map_provider_overview, ((dialog, which) -> {
+ String url = "https://github.com/mapsforge/mapsforge/blob/master/docs/Mapsforge-Maps.md";
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
+ }))
+ .setCancelable(true)
+ .create()
+ .show();
}
@Override
@@ -111,16 +201,92 @@ public void onDisplayPreferenceDialog(Preference preference) {
if (dialogFragment != null) {
dialogFragment.setTargetFragment(this, 0);
- dialogFragment.show(getFragmentManager(), dialogFragment.getClass().getCanonicalName());
+ dialogFragment.show(requireFragmentManager(), dialogFragment.getClass().getCanonicalName());
} else {
super.onDisplayPreferenceDialog(preference);
}
}
- private void setAvailableMapSources(final ListPreference tileSourcePreference) {
+ private void setOfflinemapSettings() {
+ boolean offlineMapEnabled = ((SwitchPreference) findPreference(mOfflineMapSwitch)).isChecked();
+ if (offlineMapEnabled) {
+ listOfflineMapSources(findPreference(mOfflineMapSelection));
+ listOfflineStyleSources(findPreference(mOfflineThemeSelection));
+ }
+ findPreference(mOfflineThemeSelection).setEnabled(offlineMapEnabled);
+ findPreference(mOnlineMapSource).setEnabled(!offlineMapEnabled);
+
+ }
+
+ private void setSummary() {
+ // online map sources
+ listAvailableOnlineMapSources(findPreference(mOnlineMapSource));
+
+ // message refresh interval
+ Resources res = getResources();
+ int intervalMin = mSettingsRepository.getMessageRefreshIntervalMin();
+ findPreference(mKeyMessageRefreshInterval).setSummary(intervalMin > 0
+ ? res.getQuantityString(R.plurals.prefs_message_refresh_interval_min_summary,
+ intervalMin, intervalMin)
+ : getString(R.string.prefs_message_refresh_interval_min_summary_disabled));
+
+ // distance units
+ findPreference(mKeyDistanceUnit).setSummary(getString(
+ R.string.prefs_distance_unit_summary,
+ mSettingsRepository.getDistanceUnitLong()));
+ }
+
+ private void listOfflineMapSources(final MultiSelectListPreference pref) {
+ Set maps = mSettingsRepository.getAvailableOfflineMapSources();
+
+ final List sourceNames = new LinkedList<>();
+ final List sourceValues = new LinkedList<>();
+ for (String map: maps) {
+ File f = new File(map);
+ if (f.exists()) {
+ sourceNames.add(f.getName());
+ sourceValues.add(f.getAbsolutePath());
+ }
+ }
+
+ pref.setEntries(sourceNames.toArray(new CharSequence[0]));
+ pref.setEntryValues(sourceValues.toArray(new CharSequence[0]));
+ if (pref.getValues().isEmpty() && pref.getEntryValues().length > 0) {
+ // use all maps as default
+ Set values = new HashSet<>();
+ for (CharSequence cs : pref.getEntryValues()) {
+ values.add(cs.toString());
+ }
+ pref.setValues(values);
+ }
+ }
+
+ private void listOfflineStyleSources(final ListPreference pref) {
+ List themes = mSettingsRepository.getAvailableOfflineThemeSources();
+
+ final List sourceNames = new LinkedList<>();
+ final List sourceValues = new LinkedList<>();
+ Gson gson = new Gson();
+ for (MapsForgeTheme theme : themes) {
+ if (new File(theme.getFilePath()).exists() || theme.getId().equals("default_theme")) {
+ sourceNames.add(theme.getLocalizedDisplayName(Locale.getDefault().getLanguage()));
+ sourceValues.add(gson.toJson(theme));
+ }
+ }
+
+ pref.setEntries(sourceNames.toArray(new CharSequence[0]));
+ pref.setEntryValues(sourceValues.toArray(new CharSequence[0]));
+ if (pref.getValue() == null) {
+ // if user has selected nothing, use default theme
+ pref.setSummary(themes.get(themes.size() - 1).getLocalizedDisplayName(Locale.getDefault().getLanguage()));
+ pref.setValue(pref.getEntryValues()[0].toString());
+ }
+ }
+
+ private void listAvailableOnlineMapSources(final ListPreference tileSourcePreference) {
tileSourcePreference.setSummary(getString(
R.string.prefs_tile_source_summary,
- mSettingsRepository.getTileSourceStr()));
+ mSettingsRepository.getOnlineMapSourceStr()));
final List tileSourceNames = new LinkedList<>();
final List tileSourceValues = new LinkedList<>();
final List tileSources = TileSourceFactory.getTileSources();
@@ -133,7 +299,7 @@ private void setAvailableMapSources(final ListPreference tileSourcePreference) {
TileSourceFactory.ChartbundleWAC);
tileSources.removeAll(blacklisted_sources);
- // Some maps are only available in the US
+ // Some maps only cover the US
List usOnlySources = Arrays.asList(
TileSourceFactory.USGS_SAT,
TileSourceFactory.USGS_TOPO);
@@ -156,9 +322,9 @@ private void setAvailableMapSources(final ListPreference tileSourcePreference) {
}
CharSequence[] tileSourceNamesCS =
- tileSourceNames.toArray(new CharSequence[tileSourceNames.size()]);
+ tileSourceNames.toArray(new CharSequence[0]);
CharSequence[] tileSourceValuesCS =
- tileSourceValues.toArray(new CharSequence[tileSourceValues.size()]);
+ tileSourceValues.toArray(new CharSequence[0]);
tileSourcePreference.setEntries(tileSourceNamesCS);
tileSourcePreference.setEntryValues(tileSourceValuesCS);
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/util/OfflineMapHelper.java b/app/src/main/java/fi/bitrite/android/ws/ui/util/OfflineMapHelper.java
new file mode 100644
index 00000000..18f8e92f
--- /dev/null
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/util/OfflineMapHelper.java
@@ -0,0 +1,202 @@
+package fi.bitrite.android.ws.ui.util;
+
+import android.content.Context;
+import android.os.Environment;
+import android.util.Xml;
+
+import org.mapsforge.map.rendertheme.XmlRenderTheme;
+import org.mapsforge.map.rendertheme.XmlRenderThemeStyleLayer;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import androidx.fragment.app.FragmentActivity;
+import fi.bitrite.android.ws.R;
+import fi.bitrite.android.ws.model.MapsForgeTheme;
+import fi.bitrite.android.ws.repository.SettingsRepository;
+import io.reactivex.disposables.Disposable;
+
+public class OfflineMapHelper {
+
+ public static final String TAG = "OfflineMapHelper";
+
+ public static String defaultMapDataDirectory(Context context) {
+ return context.getExternalFilesDir(null).toString() + "/";
+ }
+
+ public static boolean containsExistingFile(File[] files) {
+ for (File f : files) {
+ if (f.exists()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static void searchOfflineMapData(SettingsRepository settingsRepository, boolean extendedSearchScope, FragmentActivity activity) {
+ File[] startFolders;
+ if (extendedSearchScope) {
+ startFolders = new File[]{
+ new File(defaultMapDataDirectory(activity)),
+ Environment.getExternalStorageDirectory()};
+ } else {
+ startFolders = new File[]{
+ new File(defaultMapDataDirectory(activity))};
+ }
+
+ Disposable progressDisposable = ProgressDialog.create(R.string.map_search_in_progress)
+ .showDelayed(activity, 100, TimeUnit.MILLISECONDS);
+
+ List mapfiles = OfflineMapHelper.findMapsforgeMapFiles(startFolders);
+ settingsRepository.setAvailableOfflineMapSources(new HashSet<>(mapfiles));
+
+ List themes = new ArrayList<>();
+ for (File themefile : OfflineMapHelper.findMapsforgeThemeFiles(startFolders)) {
+ themes.addAll(OfflineMapHelper.getThemeStyles(themefile));
+ }
+ settingsRepository.setAvailableOfflineThemeSources(themes);
+
+ progressDisposable.dispose();
+ }
+
+ private static List findMapsforgeMapFiles(File[] rootFolders) {
+ List mapFiles = new ArrayList<>();
+ for (File file : findFilesWithExtension(new ArrayList<>(), rootFolders, ".map")) {
+ if (isMapsforgeBinaryOSM(file)) {
+ mapFiles.add(file.getAbsolutePath());
+ }
+ }
+ return mapFiles;
+ }
+
+
+ private static List findMapsforgeThemeFiles(File[] rootFolders) {
+ List mapFiles = new ArrayList<>();
+ for (File file : findFilesWithExtension(new ArrayList<>(), rootFolders, ".xml")) {
+ if (isMapsforgeThemeXML(file)) {
+ mapFiles.add(file);
+ }
+ }
+ return mapFiles;
+ }
+
+ private static List getThemeStyles(File theme) {
+ XmlPullParser parser = Xml.newPullParser();
+ try (InputStream in_s = new FileInputStream(theme)) {
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
+ parser.setInput(in_s, null);
+ return extractThemes(parser, theme.getAbsolutePath());
+ } catch (XmlPullParserException|IOException ignored) {}
+
+ return new ArrayList<>();
+ }
+
+ private static List extractThemes(XmlPullParser parser, final String filePath)
+ throws XmlPullParserException, IOException {
+
+ List themes = new ArrayList<>();
+
+ int eventType = parser.getEventType();
+ String elementName;
+ MapsForgeTheme theme = null;
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ switch (eventType) {
+ case XmlPullParser.START_TAG:
+ elementName = parser.getName();
+ if ("layer".equalsIgnoreCase(elementName)) {
+ if ("true".equalsIgnoreCase(parser.getAttributeValue(null, "visible"))) {
+ theme = new MapsForgeTheme(new File(filePath).getName(), parser.getAttributeValue(null, "id"), filePath);
+ }
+ } else if (theme != null && "name".equalsIgnoreCase(elementName)) {
+ String lang = parser.getAttributeValue(null,"lang");
+ String localizedThemeName = parser.getAttributeValue(null,"value");
+ theme.addLocalizedName(lang, localizedThemeName);
+ }
+ break;
+ case XmlPullParser.END_TAG:
+ elementName = parser.getName();
+ if ("layer".equalsIgnoreCase(elementName) && theme != null ) {
+ themes.add(theme);
+ theme = null;
+ }
+ }
+ eventType = parser.next();
+ }
+
+ return themes;
+ }
+
+ public static void setThemeStyle(XmlRenderTheme theme, String id) {
+ theme.setMenuCallback(themestyle -> {
+ String themeId = id.isEmpty() ? themestyle.getDefaultValue() : id;
+ XmlRenderThemeStyleLayer baseLayer = themestyle.getLayer(themeId);
+ if (baseLayer == null) {
+ return null;
+ }
+
+ // add the categories from overlays
+ Set result = baseLayer.getCategories();
+ for (XmlRenderThemeStyleLayer overlay : baseLayer.getOverlays()) {
+ result.addAll(overlay.getCategories());
+ }
+
+ return result;
+ });
+ }
+
+
+ private static List findFilesWithExtension(List foundFiles, File[] filesArray, String ext) {
+ if (filesArray == null) {
+ return foundFiles;
+ }
+
+ for (File file : filesArray) {
+ if (file.isDirectory()) {
+ findFilesWithExtension(foundFiles,file.listFiles(path ->
+ (path.isDirectory() || path.getName().toLowerCase().endsWith(ext))), ext);
+ } else if (file.getName().toLowerCase().endsWith(ext)) {
+ foundFiles.add(file);
+ }
+ }
+ return foundFiles;
+ }
+
+
+ private static boolean isMapsforgeBinaryOSM(File file) {
+ // https://github.com/mapsforge/mapsforge/blob/master/docs/Specification-Binary-Map-File.md
+ int magicByteSize = 20;
+ String magicByteString = "mapsforge binary OSM";
+
+ byte[] buffer = new byte[magicByteSize];
+ try (InputStream is = new FileInputStream(file.getAbsolutePath())) {
+ is.read(buffer);
+ } catch (IOException ignored) { }
+ return new String(buffer).contentEquals(magicByteString);
+ }
+
+ private static boolean isMapsforgeThemeXML(File file) {
+ String identifier = "http://mapsforge.org/renderTheme";
+ try (BufferedReader br = new BufferedReader(new FileReader(file))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ if (line.contains(identifier)) {
+ return true;
+ }
+ }
+ } catch (Exception e) {
+ return false;
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/util/UserMarkerClusterer.java b/app/src/main/java/fi/bitrite/android/ws/ui/util/UserMarkerClusterer.java
index 852fc0fc..e1b2165c 100644
--- a/app/src/main/java/fi/bitrite/android/ws/ui/util/UserMarkerClusterer.java
+++ b/app/src/main/java/fi/bitrite/android/ws/ui/util/UserMarkerClusterer.java
@@ -159,7 +159,7 @@ private String getClusterText(int bucket) {
if (bucket < BUCKETS[0]) {
return String.valueOf(bucket);
}
- return String.valueOf(bucket) + "+";
+ return bucket + "+";
}
private class TextDrawable extends Drawable {
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 543a4c6b..7478d700 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -98,7 +98,7 @@
km
mi
- Kartenstil
+ Online-Kartenstil
%1$s (nur USA)
Aktualisierungsintervall für Nachrichten
@@ -262,4 +262,8 @@
Der Server hat die Anfrage abgewiesen. Bitte aktualisiere die App.\n(%1$s)
Zu viele Loginversuche. Dieses Konto ist zur Zeit gesperrt. Bitte versuche es später noch einmal.
+ Suche nach Offline-Karten
+ Keine Offline-Karten gefunden
+ Liste von Offline-Kartenanbietern
+ Kopiere entpackte Offline-Karten nach %1$s und versuche es erneut.
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 6d1ca6ae..df0ee38d 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -92,7 +92,7 @@
Unidad de distancia
Denunciar distancias en %1$s
- Estilo de mapa
+ Origen de mapa en línea
%1$s (sólo EE.UU.)
Kilómetros
Millas
@@ -248,4 +248,8 @@
El servidor rechazó la solicitud. Por favor actualice la aplicación.\n(%1$s)
Demasiados intentos de inicio de sesión. Su cuenta está temporalmente bloqueada. Por favor, inténtelo de nuevo más tarde.
+ Lista de proveedores de mapas fuera de línea
+ No se han encontrado mapas fuera de línea
+ Copie los datos del mapa fuera de línea descomprimidos en %1$s e inténtelo de nuevo.
+ Búsqueda de mapas fuera de línea
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 554d9130..337f91b7 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -97,7 +97,7 @@
km
mi
- Style de la carte
+ Source de la carte en ligne
%1$s (É.-U. seulement)
@@ -254,4 +254,8 @@
Le serveur a rejeté la demande. STP mettre à jour l\'application.\n(%1$s)
Trop de tentatives de connexion. Votre compte est temporairement bloqué. Veuillez réessayer plus tard.
+ Copiez la carte hors ligne dans %1$s et réessayez.
+ Recherche de cartes hors ligne
+ Aucune carte hors-ligne trouvée
+ Liste des fournisseurs de cartes hors ligne
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3d50a96e..24615850 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -102,11 +102,16 @@
km
mi
- tile_source
- Map style
+ Online map source
%1$s
%1$s (US only)
+ offline_map_enabled
+ offline_map_selection
+ offline_map_source_files
+ offline_theme_selection
+ offline_theme_source_files
+
message_reload_interval_min
Messages poll frequency
@@ -274,4 +279,14 @@
Too many login attempts. Your account is temporarily blocked. Please try again later.
@string/title_fragment_messages
+ prefs_online_map_source
+ prefs_offline_maps_prefs_help_text
+ prefs_offline_map_search
+ prefs_offline_map_prefer_installed_locales
+ Searching for offline maps
+ prefs_offline_map_theme_sources
+ prefs_offline_map_search_scope
+ No offline maps found
+ Copy unzipped offline map data to %1$s and try again.
+ List of offline map providers
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index f9daff04..ae2c2355 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -5,20 +5,35 @@ Reflect changes in `app/src/google/res/xml/preferences.xml`.
-->
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
-
+
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
diff --git a/build.gradle b/build.gradle
index ca272c82..5afa3c31 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,7 +7,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.5.0'
+ classpath 'com.android.tools.build:gradle:3.5.2'
classpath 'com.saantiaguilera.securekeys:plugin:2.2.0'
// NOTE: Do not place your application dependencies here; they belong
diff --git a/gradle.properties b/gradle.properties
index 59590122..3a5e83fa 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,3 @@
android.enableJetifier=true
android.useAndroidX=true
-android.jetifier.blacklist = shadows-supportv4-4.3.jar
+android.jetifier.blacklist = shadows-supportv4-4.3.1.jar