From 6ca799033527e552b069ced02e173a01fdaf0df1 Mon Sep 17 00:00:00 2001 From: "alex.so.3" Date: Mon, 28 Sep 2026 01:39:25 +0200 Subject: [PATCH 1/3] Fall back to OpenStreetMap when the build has no Google Maps key The map screen threw at start without MAPS_API_KEY; OSM needs no key, so it now stands in for Google in both the map and the history screen. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016xeHfDLMuUeusX9PEgNEmY --- .../opentagviewer/HistoryViewActivity.java | 2 +- .../android/opentagviewer/MapsActivity.java | 11 +++++++--- .../ui/maps/MapProviderFactory.java | 22 +++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/dev/wander/android/opentagviewer/HistoryViewActivity.java b/app/src/main/java/dev/wander/android/opentagviewer/HistoryViewActivity.java index f078b0d4..26e998ef 100644 --- a/app/src/main/java/dev/wander/android/opentagviewer/HistoryViewActivity.java +++ b/app/src/main/java/dev/wander/android/opentagviewer/HistoryViewActivity.java @@ -216,7 +216,7 @@ protected void onCreate(Bundle savedInstanceState) { // Obtain the SupportMapFragment and get notified when the map is ready to be used. // 根据用户设置创建地图提供商 String mapProviderType = this.userSettings.getMapProvider(); - this.mapProvider = MapProviderFactory.create(mapProviderType); + this.mapProvider = MapProviderFactory.create(MapProviderFactory.effectiveProvider(this, mapProviderType)); // 初始化地图 this.mapProvider.initialize(this, R.id.history_map, this); diff --git a/app/src/main/java/dev/wander/android/opentagviewer/MapsActivity.java b/app/src/main/java/dev/wander/android/opentagviewer/MapsActivity.java index 0d740daf..6b68d09a 100644 --- a/app/src/main/java/dev/wander/android/opentagviewer/MapsActivity.java +++ b/app/src/main/java/dev/wander/android/opentagviewer/MapsActivity.java @@ -560,6 +560,9 @@ protected void onCreate(Bundle savedInstanceState) { // 根据用户设置创建地图提供商 String mapProviderType = this.userSettings.getMapProvider(); + // A build without a Google Maps key (secrets.properties) still has a map: OpenStreetMap + // needs no key, so it stands in for Google rather than the app refusing to start. + mapProviderType = MapProviderFactory.effectiveProvider(this, mapProviderType); // **Before the new provider touches it.** Changing provider calls recreate(), and the // FragmentManager restores Google's map fragment into this container while a plain view @@ -1751,7 +1754,8 @@ public void onClickMoreForDevice(View view) { deviceInfoActivityLauncher.launch(deviceInfoIntent); } - private void checkApiKey() { + /** Whether this build carries a Google Maps key; without one the map falls back to OSM. */ + private boolean checkApiKey() { try { ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA); Bundle bundle = Objects.requireNonNull(appInfo.metaData); @@ -1759,9 +1763,10 @@ private void checkApiKey() { String apiKey = bundle.getString("com.google.android.geo.API_KEY"); if (apiKey == null || apiKey.isBlank() || apiKey.equals("DEFAULT_API_KEY")) { - Toast.makeText(this, "API Key was not set in secrets.properties", LENGTH_SHORT).show(); - throw new RuntimeException("API Key was not set in secrets.properties"); + Log.w(TAG, "No Google Maps API key in secrets.properties; using OpenStreetMap"); + return false; } + return true; } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Package name not found.", e); throw new RuntimeException("Error getting package info", e); diff --git a/app/src/main/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactory.java b/app/src/main/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactory.java index 5710357c..3f3a0227 100644 --- a/app/src/main/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactory.java +++ b/app/src/main/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactory.java @@ -47,6 +47,28 @@ public static void reset() { replacement = null; } + /** + * The provider to actually use: Google Maps needs the key from secrets.properties, and a + * build without one gets OpenStreetMap (which needs none) instead of a map that cannot load. + */ + public static String effectiveProvider(final android.content.Context context, final String providerType) { + if (providerType != null && !PROVIDER_GOOGLE.equals(providerType)) { + return providerType; + } + try { + final android.os.Bundle meta = context.getPackageManager().getApplicationInfo( + context.getPackageName(), android.content.pm.PackageManager.GET_META_DATA).metaData; + final String key = meta == null ? null : meta.getString("com.google.android.geo.API_KEY"); + if (key == null || key.isBlank() || key.equals("DEFAULT_API_KEY")) { + return PROVIDER_OSM; + } + } catch (final Exception e) { + Log.w(TAG, "Could not read the Maps API key; using OpenStreetMap", e); + return PROVIDER_OSM; + } + return providerType; + } + public static IMapProvider create(String providerType) { if (replacement != null) { Log.d(TAG, "Creating a substituted map provider"); From 8a49cad744f1173c49beee7fc0f6ab2ba4eeead0 Mon Sep 17 00:00:00 2001 From: "Shane B." Date: Tue, 13 Oct 2026 11:21:17 +0200 Subject: [PATCH 2/3] Fall back to OpenStreetMap for CI's placeholder Maps key too A debug build from CI without the repository secret gets maps_key_default_value, which the check did not recognise, so it still drew an empty Google map. The key decision is now a pure function with a JVM test, and MapsActivity's separate checkApiKey, which repeated it, is gone. Co-Authored-By: Claude Opus 5.5 --- .../android/opentagviewer/MapsActivity.java | 25 ----------- .../ui/maps/MapProviderFactory.java | 45 +++++++++++++------ .../ui/maps/MapProviderFactoryTest.java | 42 +++++++++++++++++ 3 files changed, 74 insertions(+), 38 deletions(-) create mode 100644 app/src/test/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactoryTest.java diff --git a/app/src/main/java/dev/wander/android/opentagviewer/MapsActivity.java b/app/src/main/java/dev/wander/android/opentagviewer/MapsActivity.java index 6b68d09a..47dd2bf6 100644 --- a/app/src/main/java/dev/wander/android/opentagviewer/MapsActivity.java +++ b/app/src/main/java/dev/wander/android/opentagviewer/MapsActivity.java @@ -21,9 +21,7 @@ import android.bluetooth.le.ScanSettings; import android.content.Intent; -import android.content.pm.ApplicationInfo; import android.app.Dialog; -import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.location.Address; import android.location.Geocoder; @@ -502,7 +500,6 @@ protected void onCreate(Bundle savedInstanceState) { app.setupTheme(); WindowCompat.setDecorFitsSystemWindows(getWindow(), false); - this.checkApiKey(); this.binding = ActivityMapsBinding.inflate(getLayoutInflater()); setContentView(this.binding.getRoot()); @@ -1754,28 +1751,6 @@ public void onClickMoreForDevice(View view) { deviceInfoActivityLauncher.launch(deviceInfoIntent); } - /** Whether this build carries a Google Maps key; without one the map falls back to OSM. */ - private boolean checkApiKey() { - try { - ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA); - Bundle bundle = Objects.requireNonNull(appInfo.metaData); - - String apiKey = bundle.getString("com.google.android.geo.API_KEY"); - - if (apiKey == null || apiKey.isBlank() || apiKey.equals("DEFAULT_API_KEY")) { - Log.w(TAG, "No Google Maps API key in secrets.properties; using OpenStreetMap"); - return false; - } - return true; - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Package name not found.", e); - throw new RuntimeException("Error getting package info", e); - } catch (NullPointerException e) { - Log.e(TAG, "Error accessing meta-data.", e); // Handle the case where meta-data is completely missing - throw new RuntimeException("Error accessing meta-data in manifest", e); - } - } - private void handleAuthAndShowDevices() { var async = userAuthRepo.getUserAuth() .subscribe(this::getAppleUser, diff --git a/app/src/main/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactory.java b/app/src/main/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactory.java index 3f3a0227..22f2e5a4 100644 --- a/app/src/main/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactory.java +++ b/app/src/main/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactory.java @@ -1,9 +1,13 @@ package dev.wander.android.opentagviewer.ui.maps; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Bundle; import android.util.Log; import androidx.annotation.VisibleForTesting; +import java.util.Set; import java.util.function.Supplier; /** @@ -48,22 +52,37 @@ public static void reset() { } /** - * The provider to actually use: Google Maps needs the key from secrets.properties, and a - * build without one gets OpenStreetMap (which needs none) instead of a map that cannot load. + * Values the Maps key takes in a build that has no real one, so Google would draw an empty + * grey map. {@code DEFAULT_API_KEY} comes from {@code local.defaults.properties} when there is + * no {@code secrets.properties}; {@code maps_key_default_value} is what + * {@code build-debug.yml} writes when the repository secret is unavailable, as on fork PRs. */ - public static String effectiveProvider(final android.content.Context context, final String providerType) { - if (providerType != null && !PROVIDER_GOOGLE.equals(providerType)) { - return providerType; - } + private static final Set PLACEHOLDER_KEYS = Set.of("DEFAULT_API_KEY", "maps_key_default_value"); + + /** + * The provider to actually use. A build without a usable Google Maps key gets OpenStreetMap, + * which needs none, instead of a map that cannot load. + */ + public static String effectiveProvider(final Context context, final String providerType) { + String key = null; try { - final android.os.Bundle meta = context.getPackageManager().getApplicationInfo( - context.getPackageName(), android.content.pm.PackageManager.GET_META_DATA).metaData; - final String key = meta == null ? null : meta.getString("com.google.android.geo.API_KEY"); - if (key == null || key.isBlank() || key.equals("DEFAULT_API_KEY")) { - return PROVIDER_OSM; - } + final Bundle meta = context.getPackageManager().getApplicationInfo( + context.getPackageName(), PackageManager.GET_META_DATA).metaData; + key = meta == null ? null : meta.getString("com.google.android.geo.API_KEY"); } catch (final Exception e) { - Log.w(TAG, "Could not read the Maps API key; using OpenStreetMap", e); + Log.w(TAG, "Could not read the Maps API key; treating it as absent", e); + } + return effectiveProvider(providerType, key); + } + + /** {@link #effectiveProvider(Context, String)} once the key has been read. */ + static String effectiveProvider(final String providerType, final String googleMapsKey) { + if (PROVIDER_AMAP.equals(providerType) || PROVIDER_OSM.equals(providerType)) { + return providerType; + } + // Google, unset or unknown - all of which create() turns into Google Maps. + if (googleMapsKey == null || googleMapsKey.isBlank() || PLACEHOLDER_KEYS.contains(googleMapsKey)) { + Log.w(TAG, "No usable Google Maps key in this build; using OpenStreetMap"); return PROVIDER_OSM; } return providerType; diff --git a/app/src/test/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactoryTest.java b/app/src/test/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactoryTest.java new file mode 100644 index 00000000..e2db7571 --- /dev/null +++ b/app/src/test/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactoryTest.java @@ -0,0 +1,42 @@ +package dev.wander.android.opentagviewer.ui.maps; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class MapProviderFactoryTest { + + private static final String A_REAL_KEY = "AIzaSyExampleExampleExampleExample123"; + + @Test + public void googleWithARealKeyStaysGoogle() { + assertEquals(MapProviderFactory.PROVIDER_GOOGLE, + MapProviderFactory.effectiveProvider(MapProviderFactory.PROVIDER_GOOGLE, A_REAL_KEY)); + } + + @Test + public void googleWithoutAUsableKeyFallsBackToOpenStreetMap() { + for (final String key : new String[]{null, "", " ", "DEFAULT_API_KEY", "maps_key_default_value"}) { + assertEquals("key: " + key, MapProviderFactory.PROVIDER_OSM, + MapProviderFactory.effectiveProvider(MapProviderFactory.PROVIDER_GOOGLE, key)); + } + } + + /** create() turns an unset or unknown provider into Google, so those need the key too. */ + @Test + public void anUnsetOrUnknownProviderIsTreatedAsGoogle() { + assertEquals(MapProviderFactory.PROVIDER_OSM, + MapProviderFactory.effectiveProvider(null, "DEFAULT_API_KEY")); + assertEquals(MapProviderFactory.PROVIDER_OSM, + MapProviderFactory.effectiveProvider("bing", null)); + assertEquals("bing", MapProviderFactory.effectiveProvider("bing", A_REAL_KEY)); + } + + @Test + public void otherProvidersNeverDependOnTheGoogleKey() { + assertEquals(MapProviderFactory.PROVIDER_AMAP, + MapProviderFactory.effectiveProvider(MapProviderFactory.PROVIDER_AMAP, null)); + assertEquals(MapProviderFactory.PROVIDER_OSM, + MapProviderFactory.effectiveProvider(MapProviderFactory.PROVIDER_OSM, null)); + } +} From bba4aaf150efc6545c05d7a36fa211bd1d302ca8 Mon Sep 17 00:00:00 2001 From: "Shane B." Date: Tue, 13 Oct 2026 11:49:41 +0200 Subject: [PATCH 3/3] Name the pure Maps key check apart from its Context overload effectiveProvider(null, key) was ambiguous between the two, and the unit test did not compile. Co-Authored-By: Claude Opus 5.5 --- .../opentagviewer/ui/maps/MapProviderFactory.java | 4 ++-- .../ui/maps/MapProviderFactoryTest.java | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactory.java b/app/src/main/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactory.java index 22f2e5a4..01a018c4 100644 --- a/app/src/main/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactory.java +++ b/app/src/main/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactory.java @@ -72,11 +72,11 @@ public static String effectiveProvider(final Context context, final String provi } catch (final Exception e) { Log.w(TAG, "Could not read the Maps API key; treating it as absent", e); } - return effectiveProvider(providerType, key); + return providerFor(providerType, key); } /** {@link #effectiveProvider(Context, String)} once the key has been read. */ - static String effectiveProvider(final String providerType, final String googleMapsKey) { + static String providerFor(final String providerType, final String googleMapsKey) { if (PROVIDER_AMAP.equals(providerType) || PROVIDER_OSM.equals(providerType)) { return providerType; } diff --git a/app/src/test/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactoryTest.java b/app/src/test/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactoryTest.java index e2db7571..05c3b81f 100644 --- a/app/src/test/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactoryTest.java +++ b/app/src/test/java/dev/wander/android/opentagviewer/ui/maps/MapProviderFactoryTest.java @@ -11,14 +11,14 @@ public class MapProviderFactoryTest { @Test public void googleWithARealKeyStaysGoogle() { assertEquals(MapProviderFactory.PROVIDER_GOOGLE, - MapProviderFactory.effectiveProvider(MapProviderFactory.PROVIDER_GOOGLE, A_REAL_KEY)); + MapProviderFactory.providerFor(MapProviderFactory.PROVIDER_GOOGLE, A_REAL_KEY)); } @Test public void googleWithoutAUsableKeyFallsBackToOpenStreetMap() { for (final String key : new String[]{null, "", " ", "DEFAULT_API_KEY", "maps_key_default_value"}) { assertEquals("key: " + key, MapProviderFactory.PROVIDER_OSM, - MapProviderFactory.effectiveProvider(MapProviderFactory.PROVIDER_GOOGLE, key)); + MapProviderFactory.providerFor(MapProviderFactory.PROVIDER_GOOGLE, key)); } } @@ -26,17 +26,17 @@ public void googleWithoutAUsableKeyFallsBackToOpenStreetMap() { @Test public void anUnsetOrUnknownProviderIsTreatedAsGoogle() { assertEquals(MapProviderFactory.PROVIDER_OSM, - MapProviderFactory.effectiveProvider(null, "DEFAULT_API_KEY")); + MapProviderFactory.providerFor(null, "DEFAULT_API_KEY")); assertEquals(MapProviderFactory.PROVIDER_OSM, - MapProviderFactory.effectiveProvider("bing", null)); - assertEquals("bing", MapProviderFactory.effectiveProvider("bing", A_REAL_KEY)); + MapProviderFactory.providerFor("bing", null)); + assertEquals("bing", MapProviderFactory.providerFor("bing", A_REAL_KEY)); } @Test public void otherProvidersNeverDependOnTheGoogleKey() { assertEquals(MapProviderFactory.PROVIDER_AMAP, - MapProviderFactory.effectiveProvider(MapProviderFactory.PROVIDER_AMAP, null)); + MapProviderFactory.providerFor(MapProviderFactory.PROVIDER_AMAP, null)); assertEquals(MapProviderFactory.PROVIDER_OSM, - MapProviderFactory.effectiveProvider(MapProviderFactory.PROVIDER_OSM, null)); + MapProviderFactory.providerFor(MapProviderFactory.PROVIDER_OSM, null)); } }