diff --git a/App.tsx b/App.tsx index 505529d..08beda3 100644 --- a/App.tsx +++ b/App.tsx @@ -626,17 +626,13 @@ export default function App() { if (isProcessing) return; // On Android 10+, picking without ACCESS_MEDIA_LOCATION makes the OS hand us - // a redacted copy (EXIF GPS stripped). That silently kills Tier-1 EXIF GPS - // and the photo's own coordinates (often a different place than where the - // user stands now) get replaced by a device-GPS fallback. Request media - // read + photo-location BEFORE the pick so the copy keeps its true EXIF GPS. - let mediaOk = true; + // a redacted copy (EXIF GPS stripped), which silently kills the photo's own + // coordinates. Request media read + photo-location BEFORE the pick so the + // copy keeps its true EXIF GPS. if (useGeotag && Platform.OS === 'android') { try { - const perm = await MediaLibrary.requestPermissionsAsync(false, ['photo']); - mediaOk = perm.granted; + await MediaLibrary.requestPermissionsAsync(false, ['photo']); } catch (e) { - mediaOk = false; console.warn('Pre-pick media+photo-location permission error:', e); } } @@ -647,13 +643,14 @@ export default function App() { quality: 1, exif: true, // Android 13+ photo picker always returns a redacted cache copy with - // assetId=null — EXIF GPS stripped, MediaStore id lost → photo GPS is - // unreachable and we'd stamp the device's location instead. The legacy - // picker (ACTION_GET_CONTENT → DocumentsUI) hands back a + // assetId=null — EXIF GPS stripped, MediaStore id lost. The legacy picker + // (ACTION_GET_CONTENT → DocumentsUI) hands back a // com.android.providers.media.documents uri, so the MediaStore asset id // survives AND (with ACCESS_MEDIA_LOCATION granted) the file is not - // redacted → tier-1 EXIF / tier-2 MediaStore both work. - legacy: Platform.OS === 'android' && useGeotag, + // redacted → tier-1 EXIF / tier-2 MediaStore both work. Always on: the + // photo keeps its own EXIF GPS even when the chip is off at pick time, so + // switching the chip on later still stamps the photo's real place. + legacy: Platform.OS === 'android', }); if (!result.canceled && result.assets && result.assets.length > 0) { @@ -668,14 +665,10 @@ export default function App() { // Clear old GPS — will be replaced if the photo (or device) has location data setGpsInfo(null); - // Multi-tier GPS resolution: EXIF → MediaLibrary → device fallback. - // Device fallback only when media read was granted (else we cannot know - // whether the photo carries its own GPS — stamping the device's current - // spot over a photo from somewhere else would be wrong). - const { gps, permissionDenied } = await resolvePhotoGPS(asset, { - useGeotag, - allowDeviceFallback: useGeotag && mediaOk, - }); + // Multi-tier GPS resolution: EXIF → MediaLibrary. Never the device's own + // location — that is where the phone is now, not where the photo was + // taken, and stamping it just printed the wrong place name on the mark. + const { gps, permissionDenied } = await resolvePhotoGPS(asset, { useGeotag }); setGpsInfo(gps); if (permissionDenied) { console.warn('Media library read permission denied — photo GPS unavailable.'); diff --git a/src/utils/locationUtils.ts b/src/utils/locationUtils.ts index 7092a26..d8c658e 100644 --- a/src/utils/locationUtils.ts +++ b/src/utils/locationUtils.ts @@ -49,13 +49,12 @@ export async function getCurrentGPS(): Promise { } } -// The one place-name picker. Order is the camera's original one -// (subregion = subAdminArea = district/county first, then city, district, -// region) kept verbatim so already-working camera stamps do not shift; only -// the photo path used to drop the district term. Shared so the camera stamp -// and a photo's stamp can never disagree about the same coordinates. +// The one place-name picker: city first, because "the place" on a stamp means +// the town people would name, while subregion is Android's subAdminArea — the +// county/district, i.e. the wrong name for the same coordinates. Shared so the +// camera stamp and a photo's stamp can never disagree. export function localityName(address: Location.LocationGeocodedAddress): string | null { - const name = address.subregion || address.city || address.district || address.region; + const name = address.city || address.subregion || address.district || address.region; return name ? name.toUpperCase() : null; } diff --git a/src/utils/photoGps.ts b/src/utils/photoGps.ts index d3dc5f1..fb1a804 100644 --- a/src/utils/photoGps.ts +++ b/src/utils/photoGps.ts @@ -3,7 +3,7 @@ import * as MediaLibrary from 'expo-media-library'; import { getAssetInfoAsync } from 'expo-media-library/legacy'; import { GPSInfo } from '../types'; import { parseExifGPS, parseExifTimestamp, ExifLike } from './exifGps'; -import { getCurrentGPS, reverseGeocode } from './locationUtils'; +import { reverseGeocode } from './locationUtils'; export interface PickedPhotoAsset { uri?: string; @@ -89,12 +89,14 @@ async function resolveFromMediaLibrary( } // Resolve GPS for a picked photo: EXIF → MediaLibrary (needs ACCESS_MEDIA_LOCATION -// on Android 10+) → device GPS fallback. +// on Android 10+). A photo that carries no location gets NO stamp: the device's +// current spot is not where the photo was taken, and substituting it only ever +// shows the wrong place name. export async function resolvePhotoGPS( asset: PickedPhotoAsset, - opts: { useGeotag: boolean; allowDeviceFallback?: boolean } + opts: { useGeotag: boolean } ): Promise { - const { useGeotag, allowDeviceFallback = true } = opts; + const { useGeotag } = opts; // Tier 1 — EXIF embedded in the file (needs no extra permission) const exifCoord = parseExifGPS(asset.exif); @@ -117,13 +119,6 @@ export async function resolvePhotoGPS( console.warn('Photo GPS tier-2 skipped — no asset id/uri'); } - // Tier 3 — device fallback (photo has no location; stamp the device's current spot) - if (allowDeviceFallback) { - const deviceGps = await getCurrentGPS(); - if (deviceGps) { - return { gps: { ...deviceGps, source: 'device' } }; - } - } - + console.warn('Photo has no readable location — stamping no GPS mark'); return { gps: null }; }