summaryrefslogtreecommitdiff
path: root/android-app/app
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-05-15 22:50:30 +0000
committerClaude <noreply@anthropic.com>2026-05-15 22:50:30 +0000
commit79a175ab56e03b4e8031e48c139060a0f434c0ab (patch)
tree97f113887dd0e08df95414cc496ace524d1443a0 /android-app/app
parent8a752c98c72235677e06f17be2f7a22776c0f74e (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')
-rw-r--r--android-app/app/src/main/AndroidManifest.xml38
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt29
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}")
}
}