Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions android/car-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
73 changes: 73 additions & 0 deletions android/car-app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import com.vanniktech.maven.publish.AndroidSingleVariantLibrary

plugins {
alias libs.plugins.androidLibrary
alias libs.plugins.ktfmt
alias libs.plugins.mavenPublish
}

android {
namespace 'com.stadiamaps.ferrostar.car.app'
compileSdk {
version = release(36)
}

defaultConfig {
minSdk 26

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
coreLibraryDesugaringEnabled = true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
// For as long as we support API 25; once we can raise support to 26, all is fine
coreLibraryDesugaring libs.desugar.jdk.libs

implementation libs.androidx.ktx
implementation libs.androidx.appcompat
implementation libs.material

implementation libs.androidx.car.app
implementation libs.kotlinx.datetime
implementation libs.kotlinx.coroutines

implementation project(':core')
implementation project(':ui-formatters')
implementation project(':ui-shared')

testImplementation libs.junit
androidTestImplementation libs.androidx.test.junit
androidTestImplementation libs.androidx.test.espresso
}

mavenPublishing {
publishToMavenCentral()

if (!project.hasProperty(SKIP_SIGNING_PROPERTY)) {
signAllPublications()
}

configure(new AndroidSingleVariantLibrary("release", true, true))

apply from: "${rootProject.projectDir}/common-pom.gradle"

pom {
name = "Ferrostar CarApp"
description = "Composable map UI car app components for Ferrostar built with MapLibre"

commonPomConfig(it, true)
}
}
Empty file.
21 changes: 21 additions & 0 deletions android/car-app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.stadiamaps.ferrostar.core
package com.stadiamaps.ferrostar.car.app

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
Expand All @@ -17,6 +17,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.stadiamaps.ferrostar.core.test", appContext.packageName)
assertEquals("com.stadiamaps.ferrostar.car.app.test", appContext.packageName)
}
}
4 changes: 4 additions & 0 deletions android/car-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.stadiamaps.ferrostar.car.app.intent

import android.location.Location
import uniffi.ferrostar.GeographicCoordinate

/**
* A parsed navigation destination from an external intent.
*
* At least one of [coordinate] or [query] will be non-null when returned from
* [NavigationIntentParser].
*
* @param latitude Destination latitude, or null if only a query string is available.
* @param longitude Destination longitude, or null if only a query string is available.
* @param query Human-readable search query or place name, or null if only coordinates are
* available.
*/
data class NavigationDestination(
val latitude: Double?,
val longitude: Double?,
val query: String?
) {
/** The destination as a [Location], or null if only a query is available. */
val location: Location?
get() =
if (latitude != null && longitude != null) {
Location("").also {
it.latitude = latitude
it.longitude = longitude
}
} else {
null
}

/** A human-readable display name for this destination. */
val displayName: String
get() =
query
?: if (latitude != null && longitude != null) {
"%.4f, %.4f".format(latitude, longitude)
} else {
"Unknown location"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.stadiamaps.ferrostar.car.app.intent

import androidx.car.app.Screen

/**
* Handles a parsed [NavigationDestination] by deciding which [Screen] to present.
*
* Implement this to control what happens when a navigation intent arrives from an external app or
* voice assistant. Common patterns:
* - Navigate immediately (return a navigation screen)
* - Show a confirmation or disclaimer screen first
* - Show search results or a waypoint picker
* - Show an alert/warning for unsupported destination types
*
* Example:
* ```
* val handler = NavigationIntentHandler { destination ->
* MyNavigationScreen(carContext, destination)
* }
* ```
*/
fun interface NavigationIntentHandler {
fun handleNavigationIntent(destination: NavigationDestination): Screen
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.stadiamaps.ferrostar.car.app.intent

import android.content.Intent
import android.net.Uri
import uniffi.ferrostar.GeographicCoordinate

/**
* Parses navigation intents into [NavigationDestination] values.
*
* Supports two common URI schemes out of the box:
* - `geo:lat,lng` and `geo:0,0?q=query` — standard Android geo URIs
* - `google.navigation:q=lat,lng` and `google.navigation:q=place+name` — Google Maps URIs
*
* This class is `open` so apps can subclass to support additional URI schemes:
* ```
* class MyParser : NavigationIntentParser() {
* override fun parseUri(uri: Uri) = parseMyScheme(uri) ?: super.parseUri(uri)
* }
* ```
*/
open class NavigationIntentParser {

/** Parses a navigation [Intent] into a [NavigationDestination], or null if unrecognized. */
fun parse(intent: Intent): NavigationDestination? {
val uri = intent.data ?: return null
return parseUri(uri)
}

/** Parses a navigation [Uri] into a [NavigationDestination], or null if unrecognized. */
open fun parseUri(uri: Uri): NavigationDestination? =
when (uri.scheme) {
"geo" ->
parseGeoSsp(
coordString = uri.schemeSpecificPart?.substringBefore('?').orEmpty(),
query = uri.getQueryParameter("q"))
"google.navigation" ->
uri.getQueryParameter("q")?.let { parseGoogleNavigationSsp(it) }
else -> null
}

companion object {
/**
* Parses the coordinate and optional query parts of a `geo:` URI.
*
* @param coordString The coordinate portion (before `?`), e.g. `"37.8,-122.4"` or `"0,0"`.
* Altitude is ignored if present (e.g. `"37.8,-122.4,100"`).
* @param query The already-decoded value of the `q` parameter, if present.
*/
fun parseGeoSsp(coordString: String, query: String?): NavigationDestination? {
val coords = parseCoordinates(coordString)
// geo:0,0 is conventionally used as "no coordinates, use query instead"
val hasCoordinates = coords != null && !(coords.lat == 0.0 && coords.lng == 0.0)

return when {
hasCoordinates -> NavigationDestination(coords!!.lat, coords.lng, query)
query != null -> NavigationDestination(null, null, query)
else -> null
}
}

/**
* Parses the already-decoded `q` value from a `google.navigation:` URI.
*
* @param q The decoded value of the `q` parameter, e.g. `"37.8,-122.4"` or `"Starbucks"`.
*/
fun parseGoogleNavigationSsp(q: String): NavigationDestination? {
val coords = parseCoordinates(q)
return if (coords != null) {
NavigationDestination(coords.lat, coords.lng, null)
} else {
NavigationDestination(null, null, q)
}
}

internal fun parseCoordinates(str: String): GeographicCoordinate? {
// limit=3 so altitude (geo:lat,lng,alt per RFC 5870) is captured and ignored
val parts = str.split(",", limit = 3)
if (parts.size < 2) return null
val lat = parts[0].trim().toDoubleOrNull() ?: return null
val lng = parts[1].trim().toDoubleOrNull() ?: return null
if (lat < -90.0 || lat > 90.0 || lng < -180.0 || lng > 180.0) return null
return GeographicCoordinate(lat, lng)
}
}
}
Loading
Loading