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..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()); @@ -560,6 +557,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,26 +1751,6 @@ public void onClickMoreForDevice(View view) { deviceInfoActivityLauncher.launch(deviceInfoIntent); } - private void 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")) { - 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"); - } - } 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 5710357c..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 @@ -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; /** @@ -47,6 +51,43 @@ public static void reset() { replacement = null; } + /** + * 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. + */ + 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 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; treating it as absent", e); + } + return providerFor(providerType, key); + } + + /** {@link #effectiveProvider(Context, String)} once the key has been read. */ + static String providerFor(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; + } + public static IMapProvider create(String providerType) { if (replacement != null) { Log.d(TAG, "Creating a substituted map provider"); 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..05c3b81f --- /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.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.providerFor(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.providerFor(null, "DEFAULT_API_KEY")); + assertEquals(MapProviderFactory.PROVIDER_OSM, + MapProviderFactory.providerFor("bing", null)); + assertEquals("bing", MapProviderFactory.providerFor("bing", A_REAL_KEY)); + } + + @Test + public void otherProvidersNeverDependOnTheGoogleKey() { + assertEquals(MapProviderFactory.PROVIDER_AMAP, + MapProviderFactory.providerFor(MapProviderFactory.PROVIDER_AMAP, null)); + assertEquals(MapProviderFactory.PROVIDER_OSM, + MapProviderFactory.providerFor(MapProviderFactory.PROVIDER_OSM, null)); + } +}