Compare commits

...

13 Commits

Author SHA1 Message Date
map-per
6a20269819 [editor] Rename button to 'Discard changes'
Signed-off-by: map-per <map-per@gmx.de>
2025-12-18 11:52:47 +01:00
NoelClick
8cd1b41cd2 [android] Update connection failure dialog cancellation behavior
- Update the connection failure dialog in the world download view based on reviewers' feedback.

Signed-off-by: NoelClick <dev@noel.click>
2025-12-18 09:41:55 +01:00
NoelClick
2b630964d0 [android] Unify custom map server dialog logic and UI
- Add setting icon for custom map server download option.
- Extract shared custom map server dialog helper.
- Clean up based on reviewers' feedback.

Signed-off-by: NoelClick <dev@noel.click>
2025-12-18 09:41:55 +01:00
NoelClick
76ecd8209e [android] Improve custom map server dialog UX
Signed-off-by: NoelClick <dev@noel.click>
2025-12-18 09:41:55 +01:00
NoelClick
82d2932ba0 [android] Add validation for custom map download URL
- Add validation for custom map download URL.
- Re-enable advanced server button after failed or paused downloads.

Signed-off-by: NoelClick <dev@noel.click>
2025-12-18 09:41:55 +01:00
NoelClick
85540133b3 [android] Clarify custom map download URL strings
Signed-off-by: NoelClick <dev@noel.click>
2025-12-18 09:41:55 +01:00
NoelClick
6ded75de9c [core] Treat custom map download URL as CDN base, not meta server
- Update `MapFilesDownloader::LoadMetaConfig()` to skip the metaserver when a
  custom base URL is set and build MetaConfig with that URL as the only server.
- Keep the existing metaserver-based behavior when no custom base URL is set.

Signed-off-by: NoelClick <dev@noel.click>
2025-12-18 09:41:55 +01:00
NoelClick
08e8ebd434 [android] Add custom world map download URL option
- Allow setting a custom map download URL in the "world-download" screen.

Signed-off-by: NoelClick <dev@noel.click>
2025-12-18 09:41:55 +01:00
NoelClick
6c92264fb0 [android] Add custom map download URL setting
Signed-off-by: NoelClick <dev@noel.click>
2025-12-18 09:41:55 +01:00
NoelClick
4ba0fc51a5 [android] Make it possible to set a custom map download URL
Signed-off-by: NoelClick <dev@noel.click>
2025-12-18 09:41:55 +01:00
NoelClick
29c363a581 [core] Allow overriding map metaserver URL and reset downloader cache
- Add custom meta server API.
- Implement override.
- Add reset hook and implement reset.
- Expose reset from Storage.
- Let JNI set URL and reset cache.

Signed-off-by: NoelClick <dev@noel.click>
2025-12-18 09:41:55 +01:00
map-per
4973f4fed2 [editor] Lower priority for building in changeset comments
Signed-off-by: map-per <map-per@gmx.de>
2025-12-18 07:53:52 +01:00
map-per
8cffa508f3 [android] Use separate function to check for MapTooOldToEdit
Signed-off-by: map-per <map-per@gmx.de>
2025-12-17 21:07:12 +01:00
28 changed files with 405 additions and 30 deletions

View File

@@ -10,6 +10,7 @@ CoMaps contributors:
Bastian Greshake Tzovaras
clover sage
Harry Bond <me@hbond.xyz>
NoelClick
thesupertechie
vikiawv
Yannik Bloscheck

View File

@@ -18,6 +18,7 @@ import android.location.Location;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.CallSuper;
@@ -25,7 +26,15 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.view.ViewCompat;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.checkbox.MaterialCheckBox;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.android.material.textview.MaterialTextView;
import app.organicmaps.base.BaseMwmFragmentActivity;
import app.organicmaps.dialog.CustomMapServerDialog;
import app.organicmaps.downloader.MapManagerHelper;
import app.organicmaps.intent.Factory;
import app.organicmaps.sdk.Framework;
@@ -38,11 +47,7 @@ import app.organicmaps.sdk.util.StringUtils;
import app.organicmaps.util.UiUtils;
import app.organicmaps.util.Utils;
import app.organicmaps.util.WindowInsetUtils.PaddingInsetsListener;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.checkbox.MaterialCheckBox;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.android.material.textview.MaterialTextView;
import java.util.List;
import java.util.Objects;
@@ -54,6 +59,7 @@ public class DownloadResourcesLegacyActivity extends BaseMwmFragmentActivity
private MaterialTextView mTvMessage;
private LinearProgressIndicator mProgress;
private MaterialButton mBtnDownload;
private MaterialButton mBtnAdvanced;
private MaterialCheckBox mChbDownloadCountry;
private String mCurrentCountry;
@@ -267,6 +273,14 @@ public class DownloadResourcesLegacyActivity extends BaseMwmFragmentActivity
mProgress = findViewById(R.id.progressbar);
mBtnDownload = findViewById(R.id.btn_download_resources);
mChbDownloadCountry = findViewById(R.id.chb_download_country);
mBtnAdvanced = findViewById(R.id.btn_advanced);
mBtnAdvanced.setOnClickListener(v -> {
CustomMapServerDialog.show(this, url -> {
prepareFilesDownload(false);
});
});
mBtnAdvanced.setEnabled(true);
mBtnListeners = new View.OnClickListener[BTN_COUNT];
mBtnNames = new String[BTN_COUNT];
@@ -291,6 +305,11 @@ public class DownloadResourcesLegacyActivity extends BaseMwmFragmentActivity
{
mBtnDownload.setOnClickListener(mBtnListeners[action]);
mBtnDownload.setText(mBtnNames[action]);
// Allow changing server only when idle or after an error.
boolean advancedEnabled = (action == DOWNLOAD || action == TRY_AGAIN || action == RESUME);
mBtnAdvanced.setEnabled(advancedEnabled);
mBtnAdvanced.setAlpha(advancedEnabled ? 1f : 0.5f);
}
private void doDownload()
@@ -359,6 +378,9 @@ public class DownloadResourcesLegacyActivity extends BaseMwmFragmentActivity
private void finishFilesDownload(int result)
{
mBtnAdvanced.setEnabled(true);
mBtnAdvanced.setAlpha(1f);
if (result == ERR_NO_MORE_FILES)
{
// World and WorldCoasts has been downloaded, we should register maps again to correctly add them to the model.
@@ -428,12 +450,16 @@ public class DownloadResourcesLegacyActivity extends BaseMwmFragmentActivity
.setTitle(titleId)
.setMessage(messageId)
.setCancelable(true)
.setOnCancelListener((dialog) -> setAction(PAUSE))
.setOnCancelListener((dialog) -> setAction(RESUME))
.setPositiveButton(R.string.try_again,
(dialog, which) -> {
setAction(TRY_AGAIN);
onTryAgainClicked();
})
.setNegativeButton(R.string.cancel,
(dialog, which) -> {
setAction(RESUME);
})
.setOnDismissListener(dialog -> mAlertDialog = null)
.show();
}

View File

@@ -0,0 +1,86 @@
package app.organicmaps.dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.PreferenceManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import app.organicmaps.R;
import app.organicmaps.sdk.Framework;
public final class CustomMapServerDialog
{
public interface OnUrlAppliedListener
{
void onUrlApplied(@NonNull String url);
}
private CustomMapServerDialog() {}
public static void show(@NonNull Context context,
@Nullable OnUrlAppliedListener listener)
{
View dialogView = LayoutInflater.from(context)
.inflate(R.layout.dialog_custom_map_server, null);
TextInputLayout til = dialogView.findViewById(R.id.til_custom_map_server);
TextInputEditText edit = dialogView.findViewById(R.id.edit_custom_map_server);
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(context);
String current = prefs.getString(context.getString(R.string.pref_custom_map_download_url), "");
edit.setText(current);
MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(context, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.download_resources_custom_url_title)
.setMessage(R.string.download_resources_custom_url_message)
.setView(dialogView)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.save, null);
AlertDialog dialog = builder.create();
dialog.setOnShowListener(dlg -> {
Button ok = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
ok.setOnClickListener(v -> {
String url = edit.getText() != null ? edit.getText().toString().trim() : "";
if (!url.isEmpty()
&& !url.startsWith("http://")
&& !url.startsWith("https://"))
{
til.setError(context.getString(R.string.download_resources_custom_url_error_scheme));
return;
}
til.setError(null);
String normalizedUrl = Framework.normalizeServerUrl(url);
prefs.edit()
.putString(context.getString(R.string.pref_custom_map_download_url), normalizedUrl)
.apply();
// Apply to native
Framework.applyCustomMapDownloadUrl(context, normalizedUrl);
if (listener != null)
listener.onUrlApplied(normalizedUrl);
dialog.dismiss();
});
});
dialog.show();
}
}

View File

@@ -4,16 +4,23 @@ import static app.organicmaps.leftbutton.LeftButtonsHolder.DISABLE_BUTTON_CODE;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceManager;
import androidx.preference.TwoStatePreference;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import app.organicmaps.MwmApplication;
import app.organicmaps.R;
import app.organicmaps.dialog.CustomMapServerDialog;
import app.organicmaps.downloader.OnmapDownloader;
import app.organicmaps.editor.LanguagesFragment;
import app.organicmaps.editor.ProfileActivity;
@@ -35,7 +42,7 @@ import app.organicmaps.sdk.util.SharedPropertiesUtils;
import app.organicmaps.sdk.util.log.LogsManager;
import app.organicmaps.util.ThemeSwitcher;
import app.organicmaps.util.Utils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -73,6 +80,7 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
initScreenSleepEnabledPrefsCallbacks();
initShowOnLockScreenPrefsCallbacks();
initLeftButtonPrefs();
initCustomMapDownloadUrlPrefsCallbacks();
}
private void initLeftButtonPrefs()
@@ -535,6 +543,34 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
});
}
private void initCustomMapDownloadUrlPrefsCallbacks()
{
Preference customUrlPref = getPreference(getString(R.string.pref_custom_map_download_url));
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
String current = prefs.getString(getString(R.string.pref_custom_map_download_url), "");
String normalizedUrl = Framework.normalizeServerUrl(current);
// Initial summary
customUrlPref.setSummary(normalizedUrl.isEmpty()
? getString(R.string.download_resources_custom_url_summary_none)
: normalizedUrl);
// Sync native
Framework.applyCustomMapDownloadUrl(requireContext(), normalizedUrl);
// Show dialog
customUrlPref.setOnPreferenceClickListener(preference -> {
CustomMapServerDialog.show(requireContext(), url -> {
preference.setSummary(url.isEmpty()
? getString(R.string.download_resources_custom_url_summary_none)
: url);
});
return true;
});
}
private void removePreference(@NonNull String categoryKey, @NonNull Preference preference)
{
final PreferenceCategory category = getPreference(categoryKey);

View File

@@ -700,22 +700,27 @@ public class PlacePageView extends Fragment
if (shouldEnableEditPlace)
{
mTvEditPlace.setEnabled(true);
mTvAddPlace.setEnabled(true);
mTvEditPlace.setOnClickListener(this);
mTvAddPlace.setOnClickListener(this);
}
else
{
mTvEditPlace.setOnClickListener((v) -> {
Utils.showSnackbar(v.getContext(), v.getRootView(), R.string.place_page_too_old_to_edit);
});
mTvAddPlace.setOnClickListener((v) -> {
Utils.showSnackbar(v.getContext(), v.getRootView(), R.string.place_page_too_old_to_edit);
});
String countryId = MapManager.nativeGetSelectedCountry();
if (countryId != null)
if (countryId != null && MapManager.nativeIsMapTooOldToEdit(countryId))
{
// map editing is disabled because the map is too old
mTvEditPlace.setEnabled(true);
mTvAddPlace.setEnabled(true);
mTvEditPlace.setOnClickListener((v) -> {
Utils.showSnackbar(v.getContext(), v.getRootView(), R.string.place_page_too_old_to_edit);
});
mTvAddPlace.setOnClickListener((v) -> {
Utils.showSnackbar(v.getContext(), v.getRootView(), R.string.place_page_too_old_to_edit);
});
CountryItem map = CountryItem.fill(countryId);
if (map.status == CountryItem.STATUS_UPDATABLE || map.status == CountryItem.STATUS_DONE
@@ -740,6 +745,12 @@ public class PlacePageView extends Fragment
mapTooOldDescription.setText(R.string.place_page_app_too_old_description);
}
}
else
{
// map editing is disabled for other reasons
mTvEditPlace.setEnabled(false);
mTvAddPlace.setEnabled(false);
}
}
final int editButtonColor =

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?colorControlNormal"
android:height="24dp"
android:width="24dp"
android:viewportHeight="960"
android:viewportWidth="960">
<path android:fillColor="@android:color/white"
android:pathData="M260,800Q169,800 104.5,737Q40,674 40,583Q40,505 87,444Q134,383 210,366Q227,294 295,229Q363,164 440,164Q473,164 496.5,187.5Q520,211 520,244L520,486L584,424L640,480L480,640L320,480L376,424L440,486L440,244Q364,258 322,317.5Q280,377 280,440L260,440Q202,440 161,481Q120,522 120,580Q120,638 161,679Q202,720 260,720L740,720Q782,720 811,691Q840,662 840,620Q840,578 811,549Q782,520 740,520L680,520L680,440Q680,392 658,350.5Q636,309 600,280L600,187Q674,222 717,290.5Q760,359 760,440L760,440L760,440Q829,448 874.5,499.5Q920,551 920,620Q920,695 867.5,747.5Q815,800 740,800L260,800ZM480,442Q480,442 480,442Q480,442 480,442L480,442Q480,442 480,442Q480,442 480,442L480,442Q480,442 480,442Q480,442 480,442L480,442Q480,442 480,442Q480,442 480,442Q480,442 480,442Q480,442 480,442L480,442Q480,442 480,442Q480,442 480,442Q480,442 480,442Q480,442 480,442L480,442L480,442Q480,442 480,442Q480,442 480,442Z"/>
</vector>

View File

@@ -11,6 +11,18 @@
android:layout_height="match_parent"
android:layout_above="@+id/button_container"
android:layout_gravity="center">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_advanced"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_settings"
app:iconTint="?iconTint"
android:contentDescription="@string/download_resources_custom_url_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="@dimen/margin_half" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textfield.TextInputLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/til_custom_map_server"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/margin_base"
android:paddingEnd="@dimen/margin_base"
android:paddingTop="@dimen/margin_base"
android:paddingBottom="@dimen/margin_half"
android:hint="@string/download_resources_custom_url_title"
app:placeholderText="@string/download_resources_custom_url_hint"
app:endIconMode="clear_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_custom_map_server"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>

View File

@@ -43,6 +43,8 @@
<string name="pref_backup" translatable="false">Backup</string>
<string name="pref_left_button" translatable="false">LeftButton</string>
<string name="pref_power" translatable="false">pref_power</string>
<string name="pref_custom_map_download_url" translatable="false">CustomMapDownloadUrl</string>
<string name="pref_advanced" translatable="false">pref_advanced</string>
<string name="notification_ticker_ltr" translatable="false">%1$s: %2$s</string>
<string name="notification_ticker_rtl" translatable="false">%2$s :%1$s</string>

View File

@@ -544,7 +544,7 @@
<string name="placepage_edit_bookmark_button">Edit Bookmark</string>
<string name="placepage_personal_notes_hint">Personal notes (text or html)</string>
<string name="editor_reset_edits_message">Discard all local changes?</string>
<string name="editor_reset_edits_button">Discard</string>
<string name="editor_reset_edits_button">Discard changes</string>
<string name="editor_remove_place_message">Delete added place?</string>
<string name="editor_remove_place_button">Delete</string>
<string name="editor_place_doesnt_exist">Place does not exist</string>
@@ -966,4 +966,12 @@
<string name="offline_explanation_title">Offline Maps</string>
<string name="offline_explanation_text">A map needs to be downloaded to view and navigate the area.\nDownload maps for areas you want to travel.</string>
<string name="list_description_empty">Edit the list to add a description</string>
<!-- Settings: Advanced group -->
<string name="advanced">Advanced</string>
<!-- Custom Download URL -->
<string name="download_resources_custom_url_title">Custom Map Server</string>
<string name="download_resources_custom_url_message">Override the default map download server used for map downloads. Leave empty to use CoMaps default server.</string>
<string name="download_resources_custom_url_hint">https://cdn-fi-1.comaps.app/</string>
<string name="download_resources_custom_url_summary_none">Not set</string>
<string name="download_resources_custom_url_error_scheme">Please enter a full URL starting with https:// and ending with /</string>
</resources>

View File

@@ -144,7 +144,8 @@
</intent>
</PreferenceScreen>
</androidx.preference.PreferenceCategory>
<PreferenceCategory
<PreferenceCategory
android:key="@string/pref_power"
android:title="@string/power_management"
android:order="4">
@@ -190,7 +191,8 @@
android:defaultValue="true"
android:widgetLayout="@layout/preference_switch"
android:order="5"/>
</PreferenceCategory>
</PreferenceCategory>
<androidx.preference.PreferenceCategory
android:key="@string/pref_privacy"
android:title="@string/privacy"
@@ -212,5 +214,18 @@
android:defaultValue="true"
android:widgetLayout="@layout/preference_switch"
android:order="2"/>
</androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory
android:key="@string/pref_advanced"
android:title="@string/advanced"
android:order="6">
<Preference
android:key="@string/pref_custom_map_download_url"
android:title="@string/download_resources_custom_url_title"
app:icon="@drawable/ic_cloud_download"
android:order="1" />
</androidx.preference.PreferenceCategory>
</androidx.preference.PreferenceScreen>

View File

@@ -124,6 +124,7 @@ dependencies {
implementation libs.androidx.lifecycle.process
implementation libs.androidx.media
implementation libs.androidx.recyclerview
implementation libs.androidx.preference
implementation libs.android.material
testImplementation libs.junit

View File

@@ -42,12 +42,16 @@ using namespace std::placeholders;
namespace
{
std::unique_ptr<MapFilesDownloader> & LegacyDownloader()
{
static auto downloader = storage::GetDownloader();
return downloader;
}
static std::vector<platform::CountryFile> g_filesToDownload;
static int g_totalDownloadedBytes;
static int g_totalBytesToDownload;
static std::shared_ptr<HttpRequest> g_currentRequest;
} // namespace
extern "C"
@@ -152,11 +156,11 @@ JNIEXPORT jint JNICALL Java_app_organicmaps_sdk_DownloadResourcesLegacyActivity_
return ERR_NO_MORE_FILES;
/// @todo One downloader instance with cached servers. All this routine will be refactored some time.
static auto downloader = storage::GetDownloader();
auto & downloader = LegacyDownloader();
storage::Storage const & storage = g_framework->GetStorage();
downloader->SetDataVersion(storage.GetCurrentDataVersion());
downloader->EnsureMetaConfigReady([&storage, ptr = jni::make_global_ref(listener)]()
downloader->EnsureMetaConfigReady([&storage, ptr = jni::make_global_ref(listener), &downloader]()
{
auto const & curFile = g_filesToDownload.back();
auto const fileName = curFile.GetFileName(MapFileType::Map);
@@ -177,4 +181,12 @@ JNIEXPORT void JNICALL Java_app_organicmaps_sdk_DownloadResourcesLegacyActivity_
LOG(LDEBUG, ("cancelCurrentFile, currentRequest=", g_currentRequest));
g_currentRequest.reset();
}
JNIEXPORT void JNICALL Java_app_organicmaps_sdk_DownloadResourcesLegacyActivity_nativeResetMetaConfig(JNIEnv *,
jclass)
{
auto & downloader = LegacyDownloader();
if (downloader)
downloader->ResetMetaConfig();
}
}

View File

@@ -1505,6 +1505,22 @@ JNIEXPORT void JNICALL Java_app_organicmaps_sdk_Framework_nativeGet3dMode(JNIEnv
env->SetBooleanField(result, buildingsField, buildings);
}
JNIEXPORT void JNICALL Java_app_organicmaps_sdk_Framework_nativeSetCustomMapDownloadUrl(JNIEnv * env, jclass,
jstring url)
{
std::string nativeUrl = jni::ToNativeString(env, url);
GetPlatform().SetCustomMapServerUrl(nativeUrl);
if (g_framework)
{
frm()->GetStorage().ResetMapDownloadMetaConfig();
}
else
{
LOG(LINFO, ("nativeSetCustomMapDownloadUrl: framework not created yet, skipping ResetMapDownloadMetaConfig"));
}
}
JNIEXPORT void JNICALL Java_app_organicmaps_sdk_Framework_nativeSetAutoZoomEnabled(JNIEnv * env, jclass,
jboolean enabled)
{

View File

@@ -587,4 +587,11 @@ JNIEXPORT jstring JNICALL Java_app_organicmaps_sdk_downloader_MapManager_nativeG
storage::CountryId const & res = g_framework->GetPlacePageInfo().GetCountryId();
return (res == storage::kInvalidCountryId ? nullptr : jni::ToJavaString(env, res));
}
// static native boolean nativeIsMapTooOldToEdit(String countryId);
JNIEXPORT jboolean JNICALL Java_app_organicmaps_sdk_downloader_MapManager_nativeIsMapTooOldToEdit(JNIEnv *env, jclass clazz,
jstring country_id)
{
return GetStorage().IsMapTooOldToEdit(jni::ToNativeString(env, country_id));
}
} // extern "C"

View File

@@ -27,4 +27,5 @@ public class DownloadResourcesLegacyActivity
public static native int nativeGetBytesToDownload();
public static native int nativeStartNextFileDownload(Listener listener);
public static native void nativeCancelCurrentFile();
public static native void nativeResetMetaConfig();
}

View File

@@ -1,10 +1,13 @@
package app.organicmaps.sdk;
import android.content.Context;
import android.graphics.Bitmap;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Size;
import app.organicmaps.sdk.api.ParsedRoutingData;
import app.organicmaps.sdk.api.ParsedSearchRequest;
import app.organicmaps.sdk.api.RequestType;
@@ -23,6 +26,7 @@ import app.organicmaps.sdk.routing.RoutingRecommendationListener;
import app.organicmaps.sdk.routing.TransitRouteInfo;
import app.organicmaps.sdk.settings.SpeedCameraMode;
import app.organicmaps.sdk.util.Constants;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -348,4 +352,20 @@ public class Framework
public static native void nativeMemoryWarning();
public static native void nativeSaveRoute();
public static native void nativeSetCustomMapDownloadUrl(String url);
public static void applyCustomMapDownloadUrl(@NonNull Context context, @Nullable String url)
{
nativeSetCustomMapDownloadUrl(normalizeServerUrl(url));
// Reset the legacy downloader too (world/coasts).
app.organicmaps.sdk.DownloadResourcesLegacyActivity.nativeResetMetaConfig();
}
public static String normalizeServerUrl(@Nullable String url)
{
String out = url != null ? url.trim() : "";
if (!out.isEmpty() && !out.endsWith("/"))
out = out + "/";
return out;
}
}

View File

@@ -2,10 +2,13 @@ package app.organicmaps.sdk;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.preference.PreferenceManager;
import app.organicmaps.sdk.bookmarks.data.BookmarkManager;
import app.organicmaps.sdk.bookmarks.data.Icon;
import app.organicmaps.sdk.downloader.Android7RootCertificateWorkaround;
@@ -25,6 +28,7 @@ import app.organicmaps.sdk.util.SharedPropertiesUtils;
import app.organicmaps.sdk.util.StorageUtils;
import app.organicmaps.sdk.util.log.Logger;
import app.organicmaps.sdk.util.log.LogsManager;
import java.io.IOException;
public final class OrganicMaps implements DefaultLifecycleObserver
@@ -167,6 +171,11 @@ public final class OrganicMaps implements DefaultLifecycleObserver
/* isTablet */ false);
Config.setStoragePath(writablePath);
// Use the same prefs as SettingsPrefsFragment
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
final String savedUrl = prefs.getString(mContext.getString(R.string.pref_custom_map_download_url), "");
Framework.nativeSetCustomMapDownloadUrl(savedUrl.trim());
mPlatformInitialized = true;
Logger.i(TAG, "Platform initialized");
}

View File

@@ -261,4 +261,9 @@ public final class MapManager
* Returns country ID which the current PP object points to, or {@code null}.
*/
public static native @Nullable String nativeGetSelectedCountry();
/**
* Returns true when the map exists and is too old for map editing.
*/
public static native boolean nativeIsMapTooOldToEdit(String countryId);
}

View File

@@ -3,4 +3,5 @@
<string name="pref_file_name" translatable="false">OrganicMapsPrefs</string>
<string name="pref_enable_logging" translatable="false">EnableLogging</string>
<string name="pref_emulate_bad_external_storage" translatable="false">EmulateBadExternalStorage</string>
<string name="pref_custom_map_download_url" translatable="false">CustomMapDownloadUrl</string>
</resources>

View File

@@ -730,7 +730,7 @@
"editor_edits_sent_message" = "Your note will be sent to OpenStreetMap";
"editor_comment_hint" = "Comment…";
"editor_reset_edits_message" = "Discard all local changes?";
"editor_reset_edits_button" = "Discard";
"editor_reset_edits_button" = "Discard changes";
"editor_remove_place_message" = "Delete added place?";
"editor_remove_place_button" = "Delete";
"editor_place_doesnt_exist" = "Place does not exist";

View File

@@ -34,7 +34,7 @@ std::string_view constexpr kVowels = "aeiouy";
std::string_view constexpr kMainTags[] = {"amenity", "shop", "tourism", "historic", "craft", "emergency",
"barrier", "highway", "office", "leisure", "waterway", "natural",
"place", "entrance", "building", "man_made", "healthcare", "attraction"};
"place", "entrance", "man_made", "healthcare", "attraction"};
std::string GetTypeForFeature(editor::XMLFeature const & node)
{
@@ -46,7 +46,7 @@ std::string GetTypeForFeature(editor::XMLFeature const & node)
std::string value = node.GetTagValue(key);
if (value == "yes")
return std::string{key};
else if (key == "shop" || key == "office" || key == "building" || key == "entrance" || key == "attraction")
else if (key == "shop" || key == "office" || key == "entrance" || key == "attraction")
return value.append(" ").append(key); // "convenience shop"
else if (!value.empty() && value.back() == 's')
// Remove 's' from the tail: "toilets" -> "toilet".
@@ -60,6 +60,12 @@ std::string GetTypeForFeature(editor::XMLFeature const & node)
if (node.HasTag("disused:shop") || node.HasTag("disused:amenity"))
return "vacant business";
if (node.HasTag("building"))
{
std::string value = node.GetTagValue("building");
return value == "yes" ? "building" : value.append(" building");
}
if (node.HasTag("addr:housenumber") || node.HasTag("addr:street") || node.HasTag("addr:postcode"))
return "address";

View File

@@ -150,6 +150,17 @@ std::string Platform::ReadPathForFile(std::string const & file, std::string sear
"\nr: ", m_resourcesDir, "\ns: ", m_settingsDir));
}
void Platform::SetCustomMapServerUrl(std::string & url)
{
strings::Trim(url);
m_customMapServerUrl = std::move(url);
}
std::string const & Platform::CustomMapServerUrl() const
{
return m_customMapServerUrl;
}
std::string Platform::MetaServerUrl() const
{
return METASERVER_URL;

View File

@@ -268,6 +268,10 @@ public:
/// @return integer version in yyMMdd format.
int32_t IntVersion() const;
/// Set custom map server url to allow user-defined download servers
void SetCustomMapServerUrl(std::string & url);
std::string const & CustomMapServerUrl() const;
/// @return url for clients to download maps
std::string MetaServerUrl() const;
@@ -339,6 +343,8 @@ private:
void ShutdownThreads();
void GetSystemFontNames(FilesList & res) const;
std::string m_customMapServerUrl;
};
std::string DebugPrint(Platform::EError err);

View File

@@ -156,6 +156,18 @@ std::string GetAcceptLanguage()
MetaConfig MapFilesDownloader::LoadMetaConfig()
{
Platform & pl = GetPlatform();
// If user sets a custom download server, skip metaserver entirely.
std::string const customServer = pl.CustomMapServerUrl();
if (!customServer.empty())
{
LOG(LINFO, ("Using custom map server URL:", customServer));
MetaConfig metaConfig;
metaConfig.m_serversList = {customServer};
return metaConfig;
}
std::string const metaServerUrl = pl.MetaServerUrl();
std::string httpResult;
@@ -170,7 +182,7 @@ MetaConfig MapFilesDownloader::LoadMetaConfig()
request.RunHttpRequest(httpResult);
}
std::optional<MetaConfig> metaConfig = downloader::ParseMetaConfig(httpResult);
auto metaConfig = downloader::ParseMetaConfig(httpResult);
if (!metaConfig)
{
metaConfig = downloader::ParseMetaConfig(pl.DefaultUrlsJSON());
@@ -181,6 +193,7 @@ MetaConfig MapFilesDownloader::LoadMetaConfig()
{
LOG(LINFO, ("Got servers list:", metaConfig->m_serversList));
}
CHECK(!metaConfig->m_serversList.empty(), ());
return *metaConfig;
}
@@ -190,4 +203,10 @@ void MapFilesDownloader::GetMetaConfig(MetaConfigCallback const & callback)
callback(LoadMetaConfig());
}
void MapFilesDownloader::ResetMetaConfig()
{
m_serversList.clear();
m_isMetaConfigRequested = false;
}
} // namespace storage

View File

@@ -66,6 +66,9 @@ public:
void SetDownloadingPolicy(DownloadingPolicy * policy);
void SetDataVersion(int64_t version) { m_dataVersion = version; }
/// Reset after changes, e.g. map download URL.
void ResetMetaConfig();
/// @name Legacy functions for Android resources downloading routine (initial World download).
/// @{
void EnsureMetaConfigReady(std::function<void()> && callback);

View File

@@ -1252,16 +1252,33 @@ bool Storage::IsAllowedToEditVersion(CountryId const & countryId) const
case Status::OnDiskOutOfDate:
{
auto const localFile = GetLatestLocalFile(countryId);
ASSERT(localFile, ("Local file shouldn't be nullptr."));
auto const currentVersionTime = base::YYMMDDToSecondsSinceEpoch(static_cast<uint32_t>(m_currentVersion));
auto const localVersionTime = base::YYMMDDToSecondsSinceEpoch(static_cast<uint32_t>(localFile->GetVersion()));
return currentVersionTime - localVersionTime < kMaxSecondsTillLastVersionUpdate &&
base::SecondsSinceEpoch() - localVersionTime < kMaxSecondsTillNoEdits;
return IsAllowedToEditFile(localFile);
}
default: return false;
}
}
bool Storage::IsMapTooOldToEdit(CountryId const & countryId) const
{
auto const status = CountryStatusEx(countryId);
if (status == Status::OnDiskOutOfDate)
{
LocalFilePtr const localFile = GetLatestLocalFile(countryId);
return !IsAllowedToEditFile(localFile);
}
return false;
}
bool Storage::IsAllowedToEditFile(LocalFilePtr const & localFile) const
{
ASSERT(localFile, ("Local file shouldn't be nullptr."));
auto const currentVersionTime = base::YYMMDDToSecondsSinceEpoch(static_cast<uint32_t>(m_currentVersion));
auto const localVersionTime = base::YYMMDDToSecondsSinceEpoch(static_cast<uint32_t>(localFile->GetVersion()));
return currentVersionTime - localVersionTime < kMaxSecondsTillLastVersionUpdate &&
base::SecondsSinceEpoch() - localVersionTime < kMaxSecondsTillNoEdits;
}
int64_t Storage::GetVersion(CountryId const & countryId) const
{
CHECK_THREAD_CHECKER(m_threadChecker, ());

View File

@@ -383,6 +383,9 @@ public:
/// \brief Returns true if the version of countryId can be used to update maps.
bool IsAllowedToEditVersion(CountryId const & countryId) const;
/// \brief Returns true when the map exists and is too old for map editing.
bool IsMapTooOldToEdit(CountryId const & countryId) const;
/// Returns version of downloaded mwm or zero.
int64_t GetVersion(CountryId const & countryId) const;
@@ -582,6 +585,12 @@ public:
void RestoreDownloadQueue();
void ResetMapDownloadMetaConfig()
{
if (m_downloader)
m_downloader->ResetMetaConfig();
}
protected:
void OnFinishDownloading();
@@ -619,6 +628,8 @@ private:
// Returns a path to a place on disk downloader can use for downloaded files.
std::string GetFileDownloadPath(CountryId const & countryId, MapFileType file) const;
bool IsAllowedToEditFile(LocalFilePtr const & localFile) const;
/// Fast version, doesn't check if country is out of date
Status CountryStatus(CountryId const & countryId) const;