init: android project scaffold (Phase 28.1)

- Kotlin DSL build with version catalog (AGP 8.7, Kotlin 2.1, Compose BOM 2025.01)
- app module: Compose + Material 3, dark theme, network security config
- DualNetworkTestScreen — Phase 28.1 gate screen (buttons wired in 28.1.2)
- YarisApp, MainActivity, YarisOBDTheme
- ProGuard rules for Retrofit/OkHttp/Kotlinx Serialization/Coil/MapLibre/Room
- cleartext allowed for 10.42.0.1 + yaris.local
- gradle wrapper properties (needs Android Studio to generate wrapper jar)
This commit is contained in:
Mathieu Moran
2026-08-12 00:26:54 -04:00
parent 69109762a8
commit f1c83a2ee7
17 changed files with 629 additions and 4 deletions
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Internet: Pi hotspot API calls + remote server uploads -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Foreground service for uploads (28.3) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- Notification permission for upload progress (Android 13+) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Access external storage for downloaded dashcam files -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<application
android:name=".YarisApp"
android:allowBackup="false"
android:fullBackupOnly="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.YarisOBD"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.YarisOBD">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,128 @@
package com.yarisobd.app
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Phase 28.1 gate screen — proves dual-network binding works on the real device
* before building anything else.
*
* Two buttons:
* "Test Pi" → calls 10.42.0.1/api/gps/status over WiFi
* "Test Server" → calls yaris.ceaf.dev/health over cellular
*
* Both must succeed simultaneously while the phone is associated with the
* Pi's no-internet hotspot ("yaris 911").
*/
@Composable
fun DualNetworkTestScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "yaris-obd",
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Phase 28.1 — Network Binding Proof",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(32.dp))
// Pi WiFi test
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Pi Hotspot (WiFi)",
style = MaterialTheme.typography.titleMedium
)
Text(
text = "Target: 10.42.0.1/api/gps/status",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = { /* TODO: 28.1.2 — WiFi-bound OkHttp call */ },
modifier = Modifier.fillMaxWidth()
) {
Text("Test Pi")
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// Server cellular test
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Remote Server (Cellular)",
style = MaterialTheme.typography.titleMedium
)
Text(
text = "Target: yaris.ceaf.dev/health",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = { /* TODO: 28.1.2 — Cellular-bound OkHttp call */ },
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary
)
) {
Text("Test Server")
}
}
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Connect phone to \"yaris 911\" hotspot first.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@@ -0,0 +1,31 @@
package com.yarisobd.app
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.yarisobd.app.ui.theme.YarisOBDTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
YarisOBDTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
// Placeholder — replaced by nav host in 28.1.2
DualNetworkTestScreen()
}
}
}
}
}
@@ -0,0 +1,16 @@
package com.yarisobd.app
import android.app.Application
class YarisApp : Application() {
override fun onCreate() {
super.onCreate()
instance = this
}
companion object {
lateinit var instance: YarisApp
private set
}
}
@@ -0,0 +1,83 @@
package com.yarisobd.app.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
// Brand colors — dark, car-friendly. Inspired by the Pi web UI's dark sidebar.
private val YarisGreen = Color(0xFF4CAF50)
private val YarisGreenDark = Color(0xFF388E3C)
private val YarisBackground = Color(0xFF121212)
private val YarisSurface = Color(0xFF1E1E1E)
private val YarisSurfaceVariant = Color(0xFF2C2C2C)
private val DarkColorScheme = darkColorScheme(
primary = YarisGreen,
onPrimary = Color.Black,
primaryContainer = YarisGreenDark,
secondary = Color(0xFF81C784),
background = YarisBackground,
surface = YarisSurface,
surfaceContainerHigh = YarisSurfaceVariant,
onBackground = Color(0xFFE0E0E0),
onSurface = Color(0xFFE0E0E0),
onSurfaceVariant = Color(0xFFAAAAAA),
error = Color(0xFFEF5350),
)
// Light scheme exists for completeness but the app defaults to dark
private val LightColorScheme = lightColorScheme(
primary = YarisGreenDark,
onPrimary = Color.White,
primaryContainer = Color(0xFFC8E6C9),
secondary = Color(0xFF43A047),
background = Color(0xFFFAFAFA),
surface = Color.White,
surfaceContainerHigh = Color(0xFFF5F5F5),
onBackground = Color(0xFF212121),
onSurface = Color(0xFF212121),
onSurfaceVariant = Color(0xFF757575),
error = Color(0xFFD32F2F),
)
@Composable
fun YarisOBDTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false, // Off by default — use our brand colors
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.background.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
content = content
)
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">yaris-obd</string>
</resources>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.YarisOBD" parent="android:Theme.Material.NoActionBar">
<item name="android:statusBarColor">@android:color/black</item>
<item name="android:navigationBarColor">@android:color/black</item>
</style>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Allow cleartext to Pi hotspot (10.42.0.1) — local-only, no internet -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.42.0.1</domain>
<domain includeSubdomains="false">yaris.local</domain>
</domain-config>
</network-security-config>