This commit is contained in:
2026-01-19 16:45:49 +01:00
commit cc0c4ae934
62 changed files with 1780 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+62
View File
@@ -0,0 +1,62 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
}
android {
namespace = "org.pupes.mhdrunpathfinder"
compileSdk {
version = release(36)
}
defaultConfig {
applicationId = "org.pupes.mhdrunpathfinder"
minSdk = 29
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
buildConfig = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation("ovh.plrapps:mapcompose:3.2.3")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.appcompat)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}
+21
View File
@@ -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
@@ -0,0 +1,24 @@
package org.pupes.mhdrunpathfinder
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("org.pupes.mhdrunpathfinder", appContext.packageName)
}
}
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MHDRunPathfinder">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MHDRunPathfinder">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,74 @@
package org.pupes.mhdrunpathfinder
import android.app.Activity
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import ovh.plrapps.mapcompose.api.moveMarker
import java.net.URL
// OkHttpClient for making HTTP requests
val httpClient = OkHttpClient.Builder()
.build()
private val HaukMainHandler = Handler(Looper.getMainLooper())
private var HaukTrackingThread: Thread? = null
private var HaukIsTracking = false
private fun fetchHaukPosition(haukUrl: String, callee: Activity) {
try {
val url = URL(haukUrl)
val targetId = url.toString().substringAfter("?")
val request = okhttp3.Request.Builder()
.url("${url.protocol}://${url.host}/api/fetch.php?id=$targetId")
.header("User-Agent", "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android)")
.build()
httpClient.newCall(request).execute().use { response ->
var message = if (response.isSuccessful) {
response.body?.string() ?: "Empty response"
} else {
"Error: ${response.code} ${response.message}"
}
message = "{" + message.substringAfter("{");
val hauk_response = Json.decodeFromString<HaukResponse>(message)
HaukMainHandler.post {
mapState.moveMarker("target_position", x = longitudeToXNormalized(hauk_response.points.last()[1]), y = latitudeToYNormalized(hauk_response.points.last()[0]))
}
}
} catch (e: Exception) {
// Post UI update to main thread
HaukMainHandler.post {
Toast.makeText(callee, e.message ?: "Unknown error", Toast.LENGTH_LONG).show()
}
}
}
// Start the timer with a URL:
fun startHaukTracking(url: String, callee: Activity) {
if (HaukIsTracking) return
HaukIsTracking = true
HaukTrackingThread = Thread {
while (HaukIsTracking) {
fetchHaukPosition(url, callee)
try {
Thread.sleep(1000) // Wait 1 second between requests
} catch (e: InterruptedException) {
break
}
}
}.apply { start() }
}
fun stopHaukTracking() {
HaukIsTracking = false
HaukTrackingThread?.interrupt()
HaukTrackingThread = null
}
@@ -0,0 +1,25 @@
package org.pupes.mhdrunpathfinder
import kotlin.math.PI
import kotlin.math.ln
import kotlin.math.tan
fun longitudeToXNormalized(longitude: Double): Double {
return (longitude + 180.0) / 360.0
}
fun latitudeToYNormalized(latitude: Double): Double {
// Clamp latitude to Web Mercator bounds
val lat = latitude.coerceIn(-85.05112878, 85.05112878)
// Convert to radians
val latRad = lat * PI / 180.0
// Web Mercator projection formula
val mercatorY = ln(tan(PI / 4.0 + latRad / 2.0))
// Normalize to 0.0 - 1.0 range
// The mercator Y range is approximately -PI to PI
return 0.5 - (mercatorY / (2.0 * PI))
}
@@ -0,0 +1,162 @@
package org.pupes.mhdrunpathfinder
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.core.app.ActivityCompat
import ovh.plrapps.mapcompose.api.moveMarker
// Handler for main thread operations
private val LocationMainHandler = Handler(Looper.getMainLooper())
private var locationManager: LocationManager? = null
private var locationListener: LocationListener? = null
private var isLocationTracking = false
// Location update configuration
private const val MIN_TIME_BETWEEN_UPDATES = 1000L // 1 second in milliseconds
private const val MIN_DISTANCE_CHANGE = 0f // 0 meters - update on any movement
// LocationListener callback
private val createLocationListener: (Activity) -> LocationListener = { callee ->
LocationListener { location ->
handleLocationUpdate(location, callee)
}
}
private fun handleLocationUpdate(location: Location, callee: Activity) {
try {
val normalizedX = longitudeToXNormalized(location.longitude)
val normalizedY = latitudeToYNormalized(location.latitude)
// Update marker on main thread
LocationMainHandler.post {
try {
mapState.moveMarker(
"current_position",
x = normalizedX,
y = normalizedY
)
} catch (e: Exception) {
// Marker might not exist yet, silently ignore
}
}
} catch (e: Exception) {
LocationMainHandler.post {
Toast.makeText(callee, "Location error: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
// Check if location permissions are granted
private fun hasLocationPermissions(context: Context): Boolean {
return ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
// Start continuous location tracking
fun startLocationTracking(callee: Activity) {
if (isLocationTracking) {
Toast.makeText(callee, "Location tracking already active", Toast.LENGTH_SHORT).show()
return
}
if (!hasLocationPermissions(callee)) {
Toast.makeText(callee, "Location permissions not granted", Toast.LENGTH_LONG).show()
// Request permissions
ActivityCompat.requestPermissions(
callee,
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
),
LOCATION_PERMISSION_REQUEST_CODE
)
return
}
try {
locationManager = callee.getSystemService(Context.LOCATION_SERVICE) as LocationManager
locationListener = createLocationListener(callee)
// Try GPS first, then network provider
val providers = listOf(
LocationManager.GPS_PROVIDER,
LocationManager.NETWORK_PROVIDER
)
var providerFound = false
for (provider in providers) {
if (locationManager?.isProviderEnabled(provider) == true) {
try {
locationManager?.requestLocationUpdates(
provider,
MIN_TIME_BETWEEN_UPDATES,
MIN_DISTANCE_CHANGE,
locationListener!!,
Looper.getMainLooper()
)
providerFound = true
// Get last known location and update immediately
locationManager?.getLastKnownLocation(provider)?.let { location ->
handleLocationUpdate(location, callee)
}
break
} catch (e: SecurityException) {
// Permission denied
Toast.makeText(callee, "Location permission denied", Toast.LENGTH_LONG).show()
return
}
}
}
if (!providerFound) {
Toast.makeText(callee, "No location provider available. Please enable GPS or network location", Toast.LENGTH_LONG).show()
return
}
isLocationTracking = true
Toast.makeText(callee, "Location tracking started", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Toast.makeText(callee, "Failed to start location tracking: ${e.message}", Toast.LENGTH_LONG).show()
}
}
// Stop location tracking
fun stopLocationTracking() {
if (!isLocationTracking) return
try {
locationListener?.let { listener ->
locationManager?.removeUpdates(listener)
}
locationListener = null
locationManager = null
isLocationTracking = false
} catch (e: Exception) {
// Silently handle cleanup errors
}
}
// Get current tracking status
fun isLocationTrackingActive(): Boolean {
return isLocationTracking
}
// Permission request code
const val LOCATION_PERMISSION_REQUEST_CODE = 1001
@@ -0,0 +1,153 @@
package org.pupes.mhdrunpathfinder
import android.R.attr.onClick
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.serialization.Serializable
import ovh.plrapps.mapcompose.api.addLayer
import ovh.plrapps.mapcompose.api.addMarker
import ovh.plrapps.mapcompose.api.scrollTo
import ovh.plrapps.mapcompose.core.TileStreamProvider
import ovh.plrapps.mapcompose.ui.MapUI
import ovh.plrapps.mapcompose.ui.state.MapState
import java.net.URL
@Serializable
data class HaukResponse(
val type: Int,
val expire: Long,
val serverTime: Double,
val interval: Int,
val points: List<List<Double>>,
val encrypted: Boolean,
val salt: String? = null
)
// MapState configuration for OpenStreetMap
// Max zoom level is 18, tile size is 256x256
// At zoom level 18, there are 2^18 = 262144 tiles in each dimension
// So fullWidth/Height = 256 * 262144
val mapState = MapState(
levelCount = 19, // zoom levels 0-18
fullWidth = 67108864, // 256 * 2^18
fullHeight = 67108864,
tileSize = 256
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
OpenStreetMapScreen()
AddButton()
}
}
}
startHaukTracking("https://hauk.limit6.eu/?M4SJ-SH7I",this)
startLocationTracking(this)
}
override fun onDestroy() {
super.onDestroy()
stopHaukTracking()
stopLocationTracking()
}
}
@Preview
@Composable
// When a composable with parameters is used with @Preview,
// a default value must be provided for the preview to render.
fun AddButton(onClick: () -> Unit = {}) {
OutlinedButton(onClick = { onClick() }) {
Text("Outlined")
}
}
@Preview
@Composable
fun OpenStreetMapScreen() {
// Create tile stream provider for OpenStreetMap with User-Agent
val tileStreamProvider = TileStreamProvider { row, col, zoomLvl ->
try {
val url = URL("https://tile.openstreetmap.org/$zoomLvl/$col/$row.png")
val connection = url.openConnection()
// Set User-Agent header as required by OSM tile usage policy
// Format: AppId/Version (Platform) (Contact: email)
val userAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android) (Contact: poliecho@pupes.org)"
connection.setRequestProperty("User-Agent", userAgent)
connection.getInputStream()
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/* Add a marker at the center of the map */
mapState.addMarker("target_position", x = longitudeToXNormalized(14.4058031), y = latitudeToYNormalized(50.0756083)) {
Icon(
painter = painterResource(id = R.drawable.target),
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = Color(0xFFFF0000)
)
}
/* Add a marker for current position */
mapState.addMarker("current_position", x = longitudeToXNormalized(14.4378), y = latitudeToYNormalized(50.0755)) {
Icon(
painter = painterResource(id = R.drawable.user_location),
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = Color(0xFF0000FF) // Blue color for current position
)
}
// Add the tile layer and set initial position
LaunchedEffect(Unit) {
mapState.addLayer(tileStreamProvider)
// Scroll to Prague, Czech Republic
// Prague coordinates: latitude 50.0755, longitude 14.4378
val normalizedX = longitudeToXNormalized(14.4378)
val normalizedY = latitudeToYNormalized(50.0755)
// Use a higher scale to zoom in more (closer to 1.0 = more zoomed in)
mapState.scrollTo(normalizedX, normalizedY, destScale = 0.8)
}
// Display the map
MapUI(
modifier = Modifier.fillMaxSize(),
state = mapState
)
}
@@ -0,0 +1,11 @@
package org.pupes.mhdrunpathfinder.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
@@ -0,0 +1,58 @@
package org.pupes.mhdrunpathfinder.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.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun MHDRunPathfinderTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
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
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@@ -0,0 +1,34 @@
package org.pupes.mhdrunpathfinder.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
+26
View File
@@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="8.4667"
android:viewportHeight="8.4667">
<path
android:pathData="M4.2333,4.2333m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:strokeWidth="0.529167"
android:fillColor="#000000"/>
<path
android:pathData="M0,3.9833h2.5v0.5h-2.5z"
android:strokeWidth="0.529167"
android:fillColor="#000000"/>
<path
android:pathData="M4.4833,5.9667l-0,2.5l-0.5,0l-0,-2.5z"
android:strokeWidth="0.529167"
android:fillColor="#000000"/>
<path
android:pathData="M8.4667,4.4833l-2.5,-0l-0,-0.5l2.5,-0z"
android:strokeWidth="0.529167"
android:fillColor="#000000"/>
<path
android:pathData="M3.9833,2.5l0,-2.5l0.5,-0l0,2.5z"
android:strokeWidth="0.529167"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="8.4667"
android:viewportHeight="8.4667">
<path
android:pathData="M4.2333,4.2333m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:strokeWidth="0.529167"
android:fillColor="#000000"/>
<path
android:pathData="M-0,1.7678l1.7678,-1.7678l0.3536,0.3536l-1.7678,1.7678z"
android:strokeWidth="0.529167"
android:fillColor="#000000"/>
<path
android:pathData="M6.6989,-0l1.7678,1.7678l-0.3536,0.3536l-1.7678,-1.7678z"
android:strokeWidth="0.529167"
android:fillColor="#000000"/>
<path
android:pathData="M8.4667,6.6989l-1.7678,1.7678l-0.3536,-0.3536l1.7678,-1.7678z"
android:strokeWidth="0.529167"
android:fillColor="#000000"/>
<path
android:pathData="M0.3536,6.3453l1.7678,1.7678l-0.3536,0.3536l-1.7678,-1.7678z"
android:strokeWidth="0.529167"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
+3
View File
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">MHD run Pathfinder</string>
</resources>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.MHDRunPathfinder" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,17 @@
package org.pupes.mhdrunpathfinder
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}