[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>
This commit is contained in:
NoelClick
2025-12-03 12:18:12 -08:00
committed by jeanbaptisteC
parent 76ecd8209e
commit 2b630964d0
15 changed files with 147 additions and 170 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

@@ -13,15 +13,11 @@ import static app.organicmaps.sdk.DownloadResourcesLegacyActivity.nativeStartNex
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
@@ -29,18 +25,16 @@ import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
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.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
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;
@@ -281,7 +275,11 @@ public class DownloadResourcesLegacyActivity extends BaseMwmFragmentActivity
mChbDownloadCountry = findViewById(R.id.chb_download_country);
mBtnAdvanced = findViewById(R.id.btn_advanced);
mBtnAdvanced.setOnClickListener(v -> openCustomServerDialog());
mBtnAdvanced.setOnClickListener(v -> {
CustomMapServerDialog.show(this, url -> {
prepareFilesDownload(false);
});
});
mBtnAdvanced.setEnabled(true);
mBtnListeners = new View.OnClickListener[BTN_COUNT];
@@ -414,56 +412,6 @@ public class DownloadResourcesLegacyActivity extends BaseMwmFragmentActivity
}
}
private void openCustomServerDialog()
{
View dialogView = getLayoutInflater().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(this);
String current = prefs.getString(getString(R.string.pref_custom_map_download_url), "");
edit.setText(current);
MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(this, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.download_resources_custom_url_title)
.setMessage(R.string.download_resources_custom_url_message)
.setView(dialogView)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.save, null);
AlertDialog dialog = builder.create();
dialog.setOnShowListener(d -> {
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(getString(R.string.download_resources_custom_url_error_scheme));
return;
}
til.setError(null);
prefs.edit()
.putString(getString(R.string.pref_custom_map_download_url), url)
.apply();
// Apply to native + reset meta configs
Framework.applyCustomMapDownloadUrl(this, url);
// Recompute total bytes (it can change with another server)
prepareFilesDownload(false);
dialog.dismiss();
});
});
dialog.show();
}
private void showErrorDialog(int result)
{
if (mAlertDialog != null && mAlertDialog.isShowing())

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

@@ -3,17 +3,13 @@ package app.organicmaps.settings;
import static app.organicmaps.leftbutton.LeftButtonsHolder.DISABLE_BUTTON_CODE;
import android.annotation.SuppressLint;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
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.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
@@ -21,11 +17,10 @@ import androidx.preference.PreferenceManager;
import androidx.preference.TwoStatePreference;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
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;
@@ -555,79 +550,25 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
String current = prefs.getString(getString(R.string.pref_custom_map_download_url), "");
String normalized = current.trim();
if (!normalized.isEmpty() && !normalized.endsWith("/"))
normalized = normalized + "/";
String normalizedUrl = Framework.normalizeServerUrl(current);
// Initial summary
customUrlPref.setSummary(normalized.isEmpty()
customUrlPref.setSummary(normalizedUrl.isEmpty()
? getString(R.string.download_resources_custom_url_summary_none)
: normalized);
: normalizedUrl);
// Sync native
Framework.applyCustomMapDownloadUrl(requireContext(), normalized);
Framework.applyCustomMapDownloadUrl(requireContext(), normalizedUrl);
// Show dialog
customUrlPref.setOnPreferenceClickListener(preference -> {
openCustomServerDialog(customUrlPref);
return true;
});
}
private void openCustomServerDialog(Preference pref)
{
View dialogView = LayoutInflater.from(requireContext())
.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(requireContext());
String current = prefs.getString(getString(R.string.pref_custom_map_download_url), "");
edit.setText(current);
MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(requireContext(), 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(getString(R.string.download_resources_custom_url_error_scheme));
return;
}
til.setError(null);
if (!url.isEmpty() && !url.endsWith("/"))
url = url + "/";
prefs.edit()
.putString(getString(R.string.pref_custom_map_download_url), url)
.apply();
// Apply native
Framework.applyCustomMapDownloadUrl(requireContext(), url);
pref.setSummary(url.isEmpty()
CustomMapServerDialog.show(requireContext(), url -> {
preference.setSummary(url.isEmpty()
? getString(R.string.download_resources_custom_url_summary_none)
: url);
dialog.dismiss();
});
return true;
});
dialog.show();
}
private void removePreference(@NonNull String categoryKey, @NonNull Preference preference)

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

@@ -17,7 +17,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_settings"
app:iconTint="?colorOnSurface"
app:iconTint="?iconTint"
android:contentDescription="@string/download_resources_custom_url_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View File

@@ -10,7 +10,8 @@
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:placeholderText="@string/download_resources_custom_url_hint"
app:endIconMode="clear_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_custom_map_server"

View File

@@ -967,13 +967,9 @@
<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>
<!-- Custom Download URL -->
<string name="pref_custom_map_download_url_title">Custom Map Server</string>
<string name="pref_custom_map_download_url_summary">Override the default map download server used for map downloads. Leave empty to use CoMaps default server.</string>
<string name="pref_custom_map_download_url_hint">https://maps.comaps.app/</string>
<string name="download_resources_advanced">Advanced…</string>
<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://maps.comaps.app/</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 http:// or https:// and ending with /.</string>
<string name="download_resources_custom_url_error_scheme">Please enter a full URL starting with https:// and ending with /</string>
</resources>

View File

@@ -214,8 +214,8 @@
android:order="2"/>
<Preference
android:key="@string/pref_custom_map_download_url"
android:title="@string/pref_custom_map_download_url_title"
android:summary="@string/pref_custom_map_download_url_summary"
android:title="@string/download_resources_custom_url_title"
app:icon="@drawable/ic_cloud_download"
android:order="3" />
</androidx.preference.PreferenceCategory>

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,16 +42,16 @@ using namespace std::placeholders;
namespace
{
std::unique_ptr<MapFilesDownloader> & LegacyDownloader()
{
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;
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"

View File

@@ -356,14 +356,16 @@ public class Framework
public static void applyCustomMapDownloadUrl(@NonNull Context context, @Nullable String url)
{
String normalizedUrl = url != null ? url.trim() : "";
// Normalize
if (!normalizedUrl.isEmpty() && !normalizedUrl.endsWith("/"))
normalizedUrl = normalizedUrl + "/";
nativeSetCustomMapDownloadUrl(normalizedUrl);
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,12 +2,13 @@ package app.organicmaps.sdk;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
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;
@@ -27,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

View File

@@ -49,11 +49,6 @@ bool GetFileTypeChecked(std::string const & path, Platform::EFileType & type)
}
} // namespace
namespace
{
std::string g_customMetaServerUrl;
} // namespace
// static
Platform::EError Platform::ErrnoToError()
{

View File

@@ -156,24 +156,16 @@ std::string GetAcceptLanguage()
MetaConfig MapFilesDownloader::LoadMetaConfig()
{
Platform & pl = GetPlatform();
std::optional<MetaConfig> metaConfig;
// 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));
// Reuse default meta settings (timeouts, other endpoints) and override servers
metaConfig = downloader::ParseMetaConfig(pl.DefaultUrlsJSON());
CHECK(metaConfig, ());
metaConfig->m_serversList.clear();
metaConfig->m_serversList.push_back(customServer);
LOG(LINFO, ("Got servers list (custom server):", metaConfig->m_serversList));
CHECK(!metaConfig->m_serversList.empty(), ());
return *metaConfig;
MetaConfig metaConfig;
metaConfig.m_serversList = {customServer};
return metaConfig;
}
std::string const metaServerUrl = pl.MetaServerUrl();
@@ -190,7 +182,7 @@ MetaConfig MapFilesDownloader::LoadMetaConfig()
request.RunHttpRequest(httpResult);
}
metaConfig = downloader::ParseMetaConfig(httpResult);
auto metaConfig = downloader::ParseMetaConfig(httpResult);
if (!metaConfig)
{
metaConfig = downloader::ParseMetaConfig(pl.DefaultUrlsJSON());
@@ -201,6 +193,7 @@ MetaConfig MapFilesDownloader::LoadMetaConfig()
{
LOG(LINFO, ("Got servers list:", metaConfig->m_serversList));
}
CHECK(!metaConfig->m_serversList.empty(), ());
return *metaConfig;
}