[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>
This commit is contained in:
NoelClick
2025-11-21 18:16:58 -08:00
committed by jeanbaptisteC
parent 85540133b3
commit 82d2932ba0
5 changed files with 79 additions and 25 deletions

View File

@@ -13,6 +13,7 @@ import static app.organicmaps.sdk.DownloadResourcesLegacyActivity.nativeStartNex
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Dialog; import android.app.Dialog;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.location.Location; import android.location.Location;
@@ -20,13 +21,25 @@ import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.widget.Button;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts; import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.view.ViewCompat; 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.base.BaseMwmFragmentActivity;
import app.organicmaps.downloader.MapManagerHelper; import app.organicmaps.downloader.MapManagerHelper;
import app.organicmaps.intent.Factory; import app.organicmaps.intent.Factory;
@@ -40,12 +53,7 @@ import app.organicmaps.sdk.util.StringUtils;
import app.organicmaps.util.UiUtils; import app.organicmaps.util.UiUtils;
import app.organicmaps.util.Utils; import app.organicmaps.util.Utils;
import app.organicmaps.util.WindowInsetUtils.PaddingInsetsListener; 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.textfield.TextInputEditText;
import com.google.android.material.textview.MaterialTextView;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -274,6 +282,7 @@ public class DownloadResourcesLegacyActivity extends BaseMwmFragmentActivity
mBtnAdvanced = findViewById(R.id.btn_advanced); mBtnAdvanced = findViewById(R.id.btn_advanced);
mBtnAdvanced.setOnClickListener(v -> openCustomServerDialog()); mBtnAdvanced.setOnClickListener(v -> openCustomServerDialog());
mBtnAdvanced.setEnabled(true);
mBtnListeners = new View.OnClickListener[BTN_COUNT]; mBtnListeners = new View.OnClickListener[BTN_COUNT];
mBtnNames = new String[BTN_COUNT]; mBtnNames = new String[BTN_COUNT];
@@ -300,7 +309,7 @@ public class DownloadResourcesLegacyActivity extends BaseMwmFragmentActivity
mBtnDownload.setText(mBtnNames[action]); mBtnDownload.setText(mBtnNames[action]);
// Allow changing server only when idle or after an error. // Allow changing server only when idle or after an error.
boolean advancedEnabled = (action == DOWNLOAD || action == TRY_AGAIN); boolean advancedEnabled = (action == DOWNLOAD || action == TRY_AGAIN || action == RESUME);
mBtnAdvanced.setEnabled(advancedEnabled); mBtnAdvanced.setEnabled(advancedEnabled);
mBtnAdvanced.setAlpha(advancedEnabled ? 1f : 0.5f); mBtnAdvanced.setAlpha(advancedEnabled ? 1f : 0.5f);
} }
@@ -371,6 +380,9 @@ public class DownloadResourcesLegacyActivity extends BaseMwmFragmentActivity
private void finishFilesDownload(int result) private void finishFilesDownload(int result)
{ {
mBtnAdvanced.setEnabled(true);
mBtnAdvanced.setAlpha(1f);
if (result == ERR_NO_MORE_FILES) if (result == ERR_NO_MORE_FILES)
{ {
// World and WorldCoasts has been downloaded, we should register maps again to correctly add them to the model. // World and WorldCoasts has been downloaded, we should register maps again to correctly add them to the model.
@@ -405,34 +417,51 @@ public class DownloadResourcesLegacyActivity extends BaseMwmFragmentActivity
private void openCustomServerDialog() private void openCustomServerDialog()
{ {
View dialogView = getLayoutInflater().inflate(R.layout.dialog_custom_map_server, null); 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); TextInputEditText edit = dialogView.findViewById(R.id.edit_custom_map_server);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
String current = prefs.getString(getString(R.string.pref_custom_map_download_url), ""); String current = prefs.getString(getString(R.string.pref_custom_map_download_url), "");
edit.setText(current); edit.setText(current);
new MaterialAlertDialogBuilder(this, R.style.MwmTheme_AlertDialog) MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(this, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.download_resources_custom_url_title) .setTitle(R.string.download_resources_custom_url_title)
.setMessage(R.string.download_resources_custom_url_message) .setMessage(R.string.download_resources_custom_url_message)
.setView(dialogView) .setView(dialogView)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.save, (dialog, which) -> { .setPositiveButton(R.string.save, null);
String url = "";
if (edit.getText() != null)
url = edit.getText().toString().trim();
// Persist for future runs + Settings screen AlertDialog dialog = builder.create();
prefs.edit() dialog.setOnShowListener(d -> {
.putString(getString(R.string.pref_custom_map_download_url), url) Button ok = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
.apply(); ok.setOnClickListener(v -> {
String url = edit.getText() != null ? edit.getText().toString().trim() : "";
// Apply to native + reset meta configs if (!url.isEmpty()
Framework.applyCustomMapDownloadUrl(this, url); && !url.startsWith("http://")
&& !url.startsWith("https://")) {
til.setError(getString(R.string.download_resources_custom_url_error_scheme));
return;
}
// Recompute total bytes (it can change with another server) til.setError(null);
prepareFilesDownload(false);
}) prefs.edit()
.show(); .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) private void showErrorDialog(int result)

View File

@@ -6,6 +6,8 @@ import android.annotation.SuppressLint;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.preference.EditTextPreference; import androidx.preference.EditTextPreference;
@@ -13,6 +15,9 @@ import androidx.preference.ListPreference;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceCategory;
import androidx.preference.TwoStatePreference; import androidx.preference.TwoStatePreference;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import app.organicmaps.MwmApplication; import app.organicmaps.MwmApplication;
import app.organicmaps.R; import app.organicmaps.R;
import app.organicmaps.downloader.OnmapDownloader; import app.organicmaps.downloader.OnmapDownloader;
@@ -36,7 +41,7 @@ import app.organicmaps.sdk.util.SharedPropertiesUtils;
import app.organicmaps.sdk.util.log.LogsManager; import app.organicmaps.sdk.util.log.LogsManager;
import app.organicmaps.util.ThemeSwitcher; import app.organicmaps.util.ThemeSwitcher;
import app.organicmaps.util.Utils; import app.organicmaps.util.Utils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -542,6 +547,16 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
EditTextPreference customUrlPref = getPreference(getString(R.string.pref_custom_map_download_url)); EditTextPreference customUrlPref = getPreference(getString(R.string.pref_custom_map_download_url));
customUrlPref.setOnPreferenceChangeListener((preference, newValue) -> { customUrlPref.setOnPreferenceChangeListener((preference, newValue) -> {
String url = newValue != null ? ((String) newValue).trim() : ""; String url = newValue != null ? ((String) newValue).trim() : "";
if (!url.isEmpty()
&& !url.startsWith("http://")
&& !url.startsWith("https://")) {
Toast.makeText(requireContext(),
R.string.download_resources_custom_url_error_scheme,
Toast.LENGTH_SHORT).show();
return false;
}
Framework.applyCustomMapDownloadUrl(requireContext(), url); Framework.applyCustomMapDownloadUrl(requireContext(), url);
return true; // save the value return true; // save the value
}); });

View File

@@ -2,6 +2,7 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/til_custom_map_server"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/download_resources_custom_url_title" android:hint="@string/download_resources_custom_url_title"

View File

@@ -974,4 +974,5 @@
<string name="download_resources_custom_url_title">Custom Map Server</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_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://maps.comaps.app/</string>
<string name="download_resources_custom_url_error_scheme">Please enter a full URL starting with http:// or https:// and ending with /.</string>
</resources> </resources>

View File

@@ -2,10 +2,12 @@ package app.organicmaps.sdk;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import androidx.annotation.Keep; import androidx.annotation.Keep;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.Size; import androidx.annotation.Size;
import app.organicmaps.sdk.api.ParsedRoutingData; import app.organicmaps.sdk.api.ParsedRoutingData;
import app.organicmaps.sdk.api.ParsedSearchRequest; import app.organicmaps.sdk.api.ParsedSearchRequest;
import app.organicmaps.sdk.api.RequestType; import app.organicmaps.sdk.api.RequestType;
@@ -24,6 +26,7 @@ import app.organicmaps.sdk.routing.RoutingRecommendationListener;
import app.organicmaps.sdk.routing.TransitRouteInfo; import app.organicmaps.sdk.routing.TransitRouteInfo;
import app.organicmaps.sdk.settings.SpeedCameraMode; import app.organicmaps.sdk.settings.SpeedCameraMode;
import app.organicmaps.sdk.util.Constants; import app.organicmaps.sdk.util.Constants;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
@@ -353,8 +356,13 @@ public class Framework
public static void applyCustomMapDownloadUrl(@NonNull Context context, @Nullable String url) public static void applyCustomMapDownloadUrl(@NonNull Context context, @Nullable String url)
{ {
String trimmed = url != null ? url.trim() : ""; String normalizedUrl = url != null ? url.trim() : "";
nativeSetCustomMapDownloadUrl(trimmed);
// Normalize
if (!normalizedUrl.isEmpty() && !normalizedUrl.endsWith("/"))
normalizedUrl = normalizedUrl + "/";
nativeSetCustomMapDownloadUrl(normalizedUrl);
// Reset the legacy downloader too (world/coasts). // Reset the legacy downloader too (world/coasts).
app.organicmaps.sdk.DownloadResourcesLegacyActivity.nativeResetMetaConfig(); app.organicmaps.sdk.DownloadResourcesLegacyActivity.nativeResetMetaConfig();
} }