diff options
| author | Claude <noreply@anthropic.com> | 2026-05-15 22:50:30 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-05-15 22:50:30 +0000 |
| commit | 79a175ab56e03b4e8031e48c139060a0f434c0ab (patch) | |
| tree | 97f113887dd0e08df95414cc496ace524d1443a0 /android-app/app/src | |
| parent | 8a752c98c72235677e06f17be2f7a22776c0f74e (diff) | |
fix: handle application/octet-stream for .gpx on Android 16 (Google Files)
Google Files uses application/octet-stream for .gpx because Android 16's
MimeTypeMap has no entry for that extension. The ExternalStorageProvider
content URI encodes the full file path, so pathSuffix=".gpx" keeps the
ACTION_VIEW filter targeted without matching all binary files.
ACTION_SEND (share sheet) filters added for typed MIME variants; code now
handles both actions — EXTRA_STREAM for SEND, intent.data for VIEW.
For octet-stream and generic XML types, OpenableColumns.DISPLAY_NAME is
checked before parsing (uri.lastPathSegment is an opaque ID for content
URIs, not the filename). All decisions logged to NavLogger.
https://claude.ai/code/session_01DNjbYxiC1cco83dArNGW3G
Diffstat (limited to 'android-app/app/src')
| -rw-r--r-- | android-app/app/src/main/AndroidManifest.xml | 38 | ||||
| -rw-r--r-- | android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt | 29 |
2 files changed, 56 insertions, 11 deletions
diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml index 196932d..a10d503 100644 --- a/android-app/app/src/main/AndroidManifest.xml +++ b/android-app/app/src/main/AndroidManifest.xml @@ -46,9 +46,7 @@ <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="application/gpx" /> </intent-filter> - <!-- GPX as XML — Android's MimeTypeMap often maps .gpx to text/xml because - application/gpx+xml isn't in the base system database. Code checks the - display name before parsing. --> + <!-- Generic XML — some file managers use this for .gpx; code checks display name --> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> @@ -59,6 +57,40 @@ <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="application/xml" /> </intent-filter> + <!-- Google Files on Android 16 sends application/octet-stream for .gpx because + MimeTypeMap has no entry for that extension. The ExternalStorageProvider URI + encodes the full file path, so pathSuffix=".gpx" keeps this targeted. + Code double-checks via OpenableColumns.DISPLAY_NAME before parsing. --> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:scheme="content" android:mimeType="application/octet-stream" + android:pathSuffix=".gpx" /> + </intent-filter> + + <!-- Share-sheet filters (ACTION_SEND): file URI is in EXTRA_STREAM, not + intent data, so pathSuffix can't help — these cover apps that type .gpx + correctly. Code checks display name for the XML variants. --> + <intent-filter> + <action android:name="android.intent.action.SEND" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:mimeType="application/gpx+xml" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.SEND" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:mimeType="application/gpx" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.SEND" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:mimeType="text/xml" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.SEND" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:mimeType="application/xml" /> + </intent-filter> </activity> </application> </manifest> diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 07adb35..bfa285d 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -140,33 +140,46 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } private fun handleIncomingGpx(intent: Intent?) { - if (intent?.action != Intent.ACTION_VIEW) return - val uri = intent.data ?: return + val action = intent?.action ?: return + // Pull the URI from the right place depending on whether this is a direct open + // (ACTION_VIEW: URI in intent.data) or a share-sheet send (ACTION_SEND: URI in EXTRA_STREAM). + val uri = when (action) { + Intent.ACTION_VIEW -> intent.data + Intent.ACTION_SEND -> intent.getParcelableExtra(Intent.EXTRA_STREAM, android.net.Uri::class.java) + else -> null + } ?: return + val mime = intent.type ?: contentResolver.getType(uri) ?: "" when { - // Known GPX MIME types — proceed immediately. mime.contains("gpx", ignoreCase = true) -> Unit - // Generic XML MIME (text/xml, application/xml): the Android system MimeTypeMap - // often lacks application/gpx+xml for .gpx, so file managers fall back to an - // XML type. Verify the display name before parsing to avoid opening random XML. - mime.contains("xml", ignoreCase = true) -> { + // Generic XML or octet-stream: confirm it's actually a .gpx file by display name + // before parsing. For content URIs from Google Files, lastPathSegment is an opaque + // ID — OpenableColumns.DISPLAY_NAME is the reliable source of the actual filename. + mime.contains("xml", ignoreCase = true) || mime == "application/octet-stream" -> { val displayName = contentResolver.query( uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null )?.use { c -> if (c.moveToFirst()) c.getString(0) else null } - if (displayName?.lowercase()?.endsWith(".gpx") != true) return + if (displayName?.lowercase()?.endsWith(".gpx") != true) { + NavLogger.i("track", "handleIncomingGpx: skipping non-gpx file (mime=$mime displayName=$displayName)") + return + } } else -> return } + + NavLogger.i("track", "handleIncomingGpx: action=$action mime=$mime uri=$uri") lifecycleScope.launch { runCatching { val points = withContext(Dispatchers.IO) { contentResolver.openInputStream(uri)?.use { GpxParser.parse(it) } ?: emptyList() } + NavLogger.i("track", "handleIncomingGpx: parsed ${points.size} points") if (points.isNotEmpty()) { viewModel.addImportedTrack(points) showOverlay(SavedTracksFragment()) } }.onFailure { e -> + NavLogger.e("track", "handleIncomingGpx: failed: ${e.javaClass.simpleName}: ${e.message}") Log.e("MainActivity", "Failed to open GPX: ${e.message}") } } |
