feat: Add Android project structure and documentation

- Add Software Design Specification (Word)
- Add Software Test Plan (Word)
- Create Android project with Kotlin + Clean Architecture
- Implement MVVM pattern with ViewModels
- Create Dashboard, Data, Map, Settings fragments
- Add custom ArtificialHorizonView for flight display
- Configure navigation and bottom navigation
- Add GPS and sensor data collection service
This commit is contained in:
2026-05-24 10:24:34 +08:00
parent 43f79dac37
commit 15c5fb4dbd
47 changed files with 1887 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'com.senstools'
compileSdk 34
defaultConfig {
applicationId "com.senstools"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
buildFeatures {
viewBinding true
}
}
dependencies {
// AndroidX Core
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// Navigation
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
// Lifecycle
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
// Room Database
implementation 'androidx.room:room-runtime:2.6.1'
implementation 'androidx.room:room-ktx:2.6.1'
annotationProcessor 'androidx.room:room-compiler:2.6.1'
// Maps
implementation 'com.google.android.gms:play-services-maps:18.2.0'
implementation 'com.google.android.gms:play-services-location:21.1.0'
// Charts
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
// DataStore
implementation 'androidx.datastore:datastore-preferences:1.0.0'
// Testing
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
+9
View File
@@ -0,0 +1,9 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /sdk/tools/proguard/proguard-android.txt
# Keep data classes
-keepclassmembers class com.senstools.domain.model.** { *; }
# Keep ViewModels
-keep class * extends androidx.lifecycle.ViewModel { *; }
+67
View File
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Location Permissions -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Sensor Permissions -->
<uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
<!-- Storage Permissions -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Network Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Foreground Service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-feature android:name="android.hardware.sensor.accelerometer" android:required="true" />
<uses-feature android:name="android.hardware.sensor.gyroscope" android:required="true" />
<uses-feature android:name="android.hardware.sensor.compass" android:required="true" />
<uses-feature android:name="android.hardware.sensor.barometer" android:required="false" />
<uses-feature android:name="android.hardware.location.gps" android:required="true" />
<application
android:name=".SensToolsApp"
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.sensTools"
tools:targetApi="31">
<activity
android:name=".presentation.ui.MainActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/Theme.sensTools">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Data Collection Service -->
<service
android:name=".data.datasource.DataCollectionService"
android:foregroundServiceType="location"
android:exported="false" />
<!-- Google Maps API Key (placeholder) -->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_API_KEY_HERE" />
</application>
</manifest>
@@ -0,0 +1,16 @@
package com.senstools
import android.app.Application
class SensToolsApp : Application() {
override fun onCreate() {
super.onCreate()
instance = this
}
companion object {
lateinit var instance: SensToolsApp
private set
}
}
@@ -0,0 +1,73 @@
package com.senstools.data.datasource
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.senstools.R
import com.senstools.presentation.ui.MainActivity
class DataCollectionService : Service() {
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): DataCollectionService = this@DataCollectionService
}
override fun onBind(intent: Intent): IBinder {
return binder
}
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(NOTIFICATION_ID, createNotification())
return START_STICKY
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Data Collection",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Sensor data collection service"
}
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
}
private fun createNotification(): Notification {
val pendingIntent = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("sensTools")
.setContentText("Recording sensor data...")
.setSmallIcon(R.drawable.ic_dashboard)
.setContentIntent(pendingIntent)
.setOngoing(true)
.build()
}
companion object {
const val CHANNEL_ID = "data_collection_channel"
const val NOTIFICATION_ID = 1
}
}
@@ -0,0 +1,9 @@
package com.senstools.domain.model
data class AppSettings(
val gpsEnabled: Boolean = true,
val sensorEnabled: Boolean = true,
val mapEnabled: Boolean = true,
val gpsRate: Int = 5,
val sensorRate: Int = 100
)
@@ -0,0 +1,8 @@
package com.senstools.domain.model
data class AttitudeData(
val pitch: Float,
val roll: Float,
val yaw: Float,
val timestamp: Long
)
@@ -0,0 +1,13 @@
package com.senstools.domain.model
data class GPSData(
val timestamp: Long,
val longitude: Double,
val latitude: Double,
val altitude: Double,
val speed: Float,
val bearing: Float,
val satellites: Int,
val time: String,
val date: String
)
@@ -0,0 +1,38 @@
package com.senstools.domain.model
data class SensorData(
val timestamp: Long,
val accelerometer: FloatArray,
val gyroscope: FloatArray,
val magnetometer: FloatArray,
val pressureAltitude: Float,
val temperature: Float,
val humidity: Float,
val time: String,
val date: String
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SensorData
if (timestamp != other.timestamp) return false
if (!accelerometer.contentEquals(other.accelerometer)) return false
if (!gyroscope.contentEquals(other.gyroscope)) return false
if (!magnetometer.contentEquals(other.magnetometer)) return false
if (pressureAltitude != other.pressureAltitude) return false
if (temperature != other.temperature) return false
if (humidity != other.humidity) return false
return true
}
override fun hashCode(): Int {
var result = timestamp.hashCode()
result = 31 * result + accelerometer.contentHashCode()
result = 31 * result + gyroscope.contentHashCode()
result = 31 * result + magnetometer.contentHashCode()
result = 31 * result + pressureAltitude.hashCode()
result = 31 * result + temperature.hashCode()
result = 31 * result + humidity.hashCode()
return result
}
}
@@ -0,0 +1,69 @@
package com.senstools.presentation.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.senstools.databinding.ItemDataFileBinding
import com.senstools.presentation.viewmodel.DataFile
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class DataFileAdapter(
private val onItemClick: (DataFile) -> Unit,
private val onExportClick: (DataFile) -> Unit,
private val onDeleteClick: (DataFile) -> Unit
) : ListAdapter<DataFile, DataFileAdapter.ViewHolder>(DataFileDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemDataFileBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class ViewHolder(
private val binding: ItemDataFileBinding
) : RecyclerView.ViewHolder(binding.root) {
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
fun bind(file: DataFile) {
binding.apply {
tvFileName.text = file.name
tvFileSize.text = formatFileSize(file.size)
tvFileDate.text = dateFormat.format(Date(file.date))
root.setOnClickListener { onItemClick(file) }
btnExport.setOnClickListener { onExportClick(file) }
btnDelete.setOnClickListener { onDeleteClick(file) }
}
}
private fun formatFileSize(size: Long): String {
return when {
size < 1024 -> "$size B"
size < 1024 * 1024 -> "${size / 1024} KB"
else -> "${size / (1024 * 1024)} MB"
}
}
}
class DataFileDiffCallback : DiffUtil.ItemCallback<DataFile>() {
override fun areItemsTheSame(oldItem: DataFile, newItem: DataFile): Boolean {
return oldItem.path == newItem.path
}
override fun areContentsTheSame(oldItem: DataFile, newItem: DataFile): Boolean {
return oldItem == newItem
}
}
}
@@ -0,0 +1,27 @@
package com.senstools.presentation.ui
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.senstools.R
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupNavigation()
}
private fun setupNavigation() {
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
findViewById<BottomNavigationView>(R.id.bottom_nav)
.setupWithNavController(navController)
}
}
@@ -0,0 +1,87 @@
package com.senstools.presentation.ui.dashboard
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.senstools.databinding.FragmentDashboardBinding
import com.senstools.presentation.viewmodel.DashboardViewModel
class DashboardFragment : Fragment() {
private var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
private val viewModel: DashboardViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupObservers()
setupListeners()
}
private fun setupObservers() {
viewModel.gpsData.observe(viewLifecycleOwner) { gps ->
binding.apply {
tvLongitude.text = String.format("%.8f", gps.longitude)
tvLatitude.text = String.format("%.8f", gps.latitude)
tvAltitude.text = String.format("%.2f m", gps.altitude)
tvSpeed.text = String.format("%.2f m/s", gps.speed)
tvBearing.text = String.format("%.2f¡ã", gps.bearing)
tvSatellites.text = gps.satellites.toString()
}
}
viewModel.sensorData.observe(viewLifecycleOwner) { sensor ->
binding.apply {
tvAccelX.text = String.format("%.2f", sensor.accelerometer[0])
tvAccelY.text = String.format("%.2f", sensor.accelerometer[1])
tvAccelZ.text = String.format("%.2f", sensor.accelerometer[2])
tvGyroX.text = String.format("%.2f", sensor.gyroscope[0])
tvGyroY.text = String.format("%.2f", sensor.gyroscope[1])
tvGyroZ.text = String.format("%.2f", sensor.gyroscope[2])
tvMagX.text = String.format("%.2f", sensor.magnetometer[0])
tvMagY.text = String.format("%.2f", sensor.magnetometer[1])
tvMagZ.text = String.format("%.2f", sensor.magnetometer[2])
tvPressure.text = String.format("%.2f m", sensor.pressureAltitude)
tvTemperature.text = String.format("%.2f¡ãC", sensor.temperature)
tvHumidity.text = String.format("%.1f%%", sensor.humidity)
}
}
viewModel.attitudeData.observe(viewLifecycleOwner) { attitude ->
binding.apply {
artificialHorizon.setAttitude(attitude.pitch, attitude.roll)
tvPitch.text = String.format("%.2f¡ã", attitude.pitch)
tvRoll.text = String.format("%.2f¡ã", attitude.roll)
tvYaw.text = String.format("%.2f¡ã", attitude.yaw)
}
}
}
private fun setupListeners() {
binding.btnStartRecording.setOnClickListener {
viewModel.startRecording()
}
binding.btnStopRecording.setOnClickListener {
viewModel.stopRecording()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,65 @@
package com.senstools.presentation.ui.data
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.senstools.databinding.FragmentDataBinding
import com.senstools.presentation.adapter.DataFileAdapter
import com.senstools.presentation.viewmodel.DataViewModel
class DataFragment : Fragment() {
private var _binding: FragmentDataBinding? = null
private val binding get() = _binding!!
private val viewModel: DataViewModel by viewModels()
private lateinit var adapter: DataFileAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentDataBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
setupObservers()
setupListeners()
}
private fun setupRecyclerView() {
adapter = DataFileAdapter(
onItemClick = { file -> viewModel.openFile(file) },
onExportClick = { file -> viewModel.exportFile(file) },
onDeleteClick = { file -> viewModel.deleteFile(file) }
)
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
}
private fun setupObservers() {
viewModel.dataFiles.observe(viewLifecycleOwner) { files ->
adapter.submitList(files)
}
}
private fun setupListeners() {
binding.swipeRefresh.setOnRefreshListener {
viewModel.refreshFiles()
binding.swipeRefresh.isRefreshing = false
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,74 @@
package com.senstools.presentation.ui.map
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.PolylineOptions
import com.senstools.databinding.FragmentMapBinding
import com.senstools.presentation.viewmodel.MapViewModel
class MapFragment : Fragment(), OnMapReadyCallback {
private var _binding: FragmentMapBinding? = null
private val binding get() = _binding!!
private val viewModel: MapViewModel by viewModels()
private var googleMap: GoogleMap? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentMapBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val mapFragment = childFragmentManager.findFragmentById(
com.senstools.R.id.mapFragment
) as? SupportMapFragment
mapFragment?.getMapAsync(this)
setupObservers()
}
override fun onMapReady(map: GoogleMap) {
googleMap = map
map.uiSettings.apply {
isZoomControlsEnabled = true
isMyLocationButtonEnabled = true
}
}
private fun setupObservers() {
viewModel.currentLocation.observe(viewLifecycleOwner) { location ->
location?.let {
val latLng = LatLng(it.latitude, it.longitude)
googleMap?.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, 17f))
}
}
viewModel.trackPoints.observe(viewLifecycleOwner) { points ->
googleMap?.clear()
if (points.isNotEmpty()) {
val polylineOptions = PolylineOptions()
.add(*points.map { LatLng(it.latitude, it.longitude) }.toTypedArray())
googleMap?.addPolyline(polylineOptions)
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,70 @@
package com.senstools.presentation.ui.settings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.senstools.databinding.FragmentSettingsBinding
import com.senstools.presentation.viewmodel.SettingsViewModel
class SettingsFragment : Fragment() {
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
private val viewModel: SettingsViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupObservers()
setupListeners()
}
private fun setupObservers() {
viewModel.settings.observe(viewLifecycleOwner) { settings ->
binding.apply {
switchGps.isChecked = settings.gpsEnabled
switchSensor.isChecked = settings.sensorEnabled
switchMap.isChecked = settings.mapEnabled
sliderGpsRate.value = settings.gpsRate.toFloat()
sliderSensorRate.value = settings.sensorRate.toFloat()
}
}
}
private fun setupListeners() {
binding.apply {
switchGps.setOnCheckedChangeListener { _, isChecked ->
viewModel.setGpsEnabled(isChecked)
}
switchSensor.setOnCheckedChangeListener { _, isChecked ->
viewModel.setSensorEnabled(isChecked)
}
switchMap.setOnCheckedChangeListener { _, isChecked ->
viewModel.setMapEnabled(isChecked)
}
sliderGpsRate.addOnChangeListener { _, value, _ ->
viewModel.setGpsRate(value.toInt())
}
sliderSensorRate.addOnChangeListener { _, value, _ ->
viewModel.setSensorRate(value.toInt())
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,184 @@
package com.senstools.presentation.ui.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.View
import kotlin.math.cos
import kotlin.math.sin
class ArtificialHorizonView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var pitch: Float = 0f // degrees
private var roll: Float = 0f // degrees
private val skyPaint = Paint().apply {
color = Color.parseColor("#1E90FF")
style = Paint.Style.FILL
}
private val groundPaint = Paint().apply {
color = Color.parseColor("#8B4513")
style = Paint.Style.FILL
}
private val horizonPaint = Paint().apply {
color = Color.WHITE
strokeWidth = 3f
style = Paint.Style.STROKE
}
private val aircraftPaint = Paint().apply {
color = Color.WHITE
strokeWidth = 4f
style = Paint.Style.STROKE
}
private val pitchMarksPaint = Paint().apply {
color = Color.WHITE
textSize = 24f
textAlign = Paint.Align.CENTER
}
private val rollIndicatorPaint = Paint().apply {
color = Color.parseColor("#FF6B00")
strokeWidth = 4f
style = Paint.Style.FILL
}
private val rollArcPaint = Paint().apply {
color = Color.WHITE
strokeWidth = 3f
style = Paint.Style.STROKE
}
fun setAttitude(pitch: Float, roll: Float) {
this.pitch = pitch
this.roll = roll
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val centerX = width / 2f
val centerY = height / 2f
val radius = minOf(width, height) / 2f - 20f
// Save canvas state
canvas.save()
// Rotate canvas for roll
canvas.rotate(-roll, centerX, centerY)
// Calculate horizon offset based on pitch
val pitchOffset = (pitch / 90f) * radius
// Draw sky (top half)
val horizonY = centerY + pitchOffset
canvas.drawRect(0f, 0f, width.toFloat(), horizonY, skyPaint)
// Draw ground (bottom half)
canvas.drawRect(0f, horizonY, width.toFloat(), height.toFloat(), groundPaint)
// Draw horizon line
canvas.drawLine(0f, horizonY, width.toFloat(), horizonY, horizonPaint)
// Draw pitch marks
drawPitchMarks(canvas, centerX, centerY, pitchOffset)
// Restore canvas
canvas.restore()
// Draw roll indicators (fixed, not affected by pitch/roll)
drawRollIndicators(canvas, centerX, centerY, radius)
// Draw aircraft reference
drawAircraftReference(canvas, centerX, centerY)
}
private fun drawPitchMarks(canvas: Canvas, centerX: Float, centerY: Float, pitchOffset: Float) {
val pitchStep = 10
val maxPitch = 90
for (deg in -maxPitch..maxPitch step pitchStep) {
if (deg == 0) continue
val y = centerY + pitchOffset - (deg / 90f) * (height / 2f)
if (y < 0 || y > height) continue
val lineWidth = if (deg % 30 == 0) 60f else 30f
canvas.drawLine(centerX - lineWidth, y, centerX + lineWidth, y, horizonPaint)
if (deg % 30 == 0) {
val text = if (deg > 0) "+$deg" else "$deg"
canvas.drawText(text, centerX - lineWidth - 40, y + 8, pitchMarksPaint)
canvas.drawText(text, centerX + lineWidth + 40, y + 8, pitchMarksPaint)
}
}
}
private fun drawRollIndicators(canvas: Canvas, centerX: Float, centerY: Float, radius: Float) {
// Draw roll arc
val arcRadius = radius * 0.8f
canvas.drawArc(
centerX - arcRadius,
centerY - arcRadius,
centerX + arcRadius,
centerY + arcRadius,
-150f, 120f, false, rollArcPaint
)
// Draw roll tick marks
for (angle in listOf(-60, -45, -30, -15, 0, 15, 30, 45, 60)) {
val rad = Math.toRadians((angle - 90).toDouble())
val innerRadius = arcRadius - 15
val outerRadius = arcRadius
val startX = centerX + (innerRadius * cos(rad)).toFloat()
val startY = centerY + (innerRadius * sin(rad)).toFloat()
val endX = centerX + (outerRadius * cos(rad)).toFloat()
val endY = centerY + (outerRadius * sin(rad)).toFloat()
canvas.drawLine(startX, startY, endX, endY, rollArcPaint)
}
// Draw roll indicator triangle
val rollRad = Math.toRadians((-roll - 90).toDouble())
val indicatorRadius = arcRadius - 5
val indicatorX = centerX + (indicatorRadius * cos(rollRad)).toFloat()
val indicatorY = centerY + (indicatorRadius * sin(rollRad)).toFloat()
val path = Path()
path.moveTo(indicatorX, indicatorY)
path.lineTo(indicatorX - 10, indicatorY - 20)
path.lineTo(indicatorX + 10, indicatorY - 20)
path.close()
canvas.drawPath(path, rollIndicatorPaint)
}
private fun drawAircraftReference(canvas: Canvas, centerX: Float, centerY: Float) {
// Draw fixed aircraft reference marks
val wingSpan = 100f
val wingHeight = 8f
// Left wing
canvas.drawLine(centerX - wingSpan, centerY, centerX - 30, centerY, aircraftPaint)
canvas.drawLine(centerX - 30, centerY, centerX - 30, centerY + wingHeight, aircraftPaint)
// Right wing
canvas.drawLine(centerX + wingSpan, centerY, centerX + 30, centerY, aircraftPaint)
canvas.drawLine(centerX + 30, centerY, centerX + 30, centerY + wingHeight, aircraftPaint)
// Center dot
canvas.drawCircle(centerX, centerY, 5f, aircraftPaint)
}
}
@@ -0,0 +1,49 @@
package com.senstools.presentation.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.senstools.domain.model.AttitudeData
import com.senstools.domain.model.GPSData
import com.senstools.domain.model.SensorData
import kotlinx.coroutines.launch
class DashboardViewModel : ViewModel() {
private val _gpsData = MutableLiveData<GPSData>()
val gpsData: LiveData<GPSData> = _gpsData
private val _sensorData = MutableLiveData<SensorData>()
val sensorData: LiveData<SensorData> = _sensorData
private val _attitudeData = MutableLiveData<AttitudeData>()
val attitudeData: LiveData<AttitudeData> = _attitudeData
private val _isRecording = MutableLiveData<Boolean>()
val isRecording: LiveData<Boolean> = _isRecording
init {
_isRecording.value = false
}
fun startRecording() {
_isRecording.value = true
}
fun stopRecording() {
_isRecording.value = false
}
fun updateGPSData(data: GPSData) {
_gpsData.value = data
}
fun updateSensorData(data: SensorData) {
_sensorData.value = data
}
fun updateAttitudeData(data: AttitudeData) {
_attitudeData.value = data
}
}
@@ -0,0 +1,43 @@
package com.senstools.presentation.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class DataViewModel : ViewModel() {
private val _dataFiles = MutableLiveData<List<DataFile>>()
val dataFiles: LiveData<List<DataFile>> = _dataFiles
init {
loadFiles()
}
private fun loadFiles() {
// Load files from storage
_dataFiles.value = emptyList()
}
fun refreshFiles() {
loadFiles()
}
fun openFile(file: DataFile) {
// Open file for viewing
}
fun exportFile(file: DataFile) {
// Export file to CSV/JSON
}
fun deleteFile(file: DataFile) {
// Delete file
}
}
data class DataFile(
val name: String,
val path: String,
val size: Long,
val date: Long
)
@@ -0,0 +1,33 @@
package com.senstools.presentation.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class MapViewModel : ViewModel() {
private val _currentLocation = MutableLiveData<LocationData?>()
val currentLocation: LiveData<LocationData?> = _currentLocation
private val _trackPoints = MutableLiveData<List<TrackPoint>>()
val trackPoints: LiveData<List<TrackPoint>> = _trackPoints
fun updateLocation(latitude: Double, longitude: Double) {
_currentLocation.value = LocationData(latitude, longitude)
_trackPoints.value = (_trackPoints.value ?: emptyList()) + TrackPoint(latitude, longitude)
}
fun clearTrack() {
_trackPoints.value = emptyList()
}
}
data class LocationData(
val latitude: Double,
val longitude: Double
)
data class TrackPoint(
val latitude: Double,
val longitude: Double
)
@@ -0,0 +1,40 @@
package com.senstools.presentation.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.senstools.domain.model.AppSettings
class SettingsViewModel : ViewModel() {
private val _settings = MutableLiveData<AppSettings>()
val settings: LiveData<AppSettings> = _settings
init {
loadSettings()
}
private fun loadSettings() {
_settings.value = AppSettings()
}
fun setGpsEnabled(enabled: Boolean) {
_settings.value = _settings.value?.copy(gpsEnabled = enabled)
}
fun setSensorEnabled(enabled: Boolean) {
_settings.value = _settings.value?.copy(sensorEnabled = enabled)
}
fun setMapEnabled(enabled: Boolean) {
_settings.value = _settings.value?.copy(mapEnabled = enabled)
}
fun setGpsRate(rate: Int) {
_settings.value = _settings.value?.copy(gpsRate = rate)
}
fun setSensorRate(rate: Int) {
_settings.value = _settings.value?.copy(sensorRate = rate)
}
}
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M3,13h8L11,3L3,3v10zM3,21h8v-6L3,15v6zM13,21h8L21,11h-8v10zM13,3v6h8L21,3h-8z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM9,17L7,17v-7h2v7zM13,17h-2L11,7h2v10zM17,17h-2v-4h2v4z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,12v7H5v-7H3v7c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2zM13,12.67l2.59,-2.58L17,11.5l-5,5l-5,-5l1.41,-1.41L11,12.67V3h2v9.67z"/>
</vector>
@@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.4"
android:scaleY="0.4"
android:translateX="32.4"
android:translateY="32.4">
<path
android:fillColor="#FFFFFF"
android:pathData="M54,27c-14.9,0 -27,12.1 -27,27s12.1,27 27,27s27,-12.1 27,-27S68.9,27 54,27zM54,75c-11.6,0 -21,-9.4 -21,-21s9.4,-21 21,-21s21,9.4 21,21S65.6,75 54,75z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M54,36c-9.9,0 -18,8.1 -18,18s8.1,18 18,18s18,-8.1 18,-18S63.9,36 54,36zM54,66c-6.6,0 -12,-5.4 -12,-12s5.4,-12 12,-12s12,5.4 12,12S60.6,66 54,66z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M54,45c-4.97,0 -9,4.03 -9,9s4.03,9 9,9s9,-4.03 9,-9S58.97,45 54,45z"/>
</group>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20.5,3l-0.16,0.03L15,5.1 9,3 3.36,4.9c-0.21,0.07 -0.36,0.25 -0.36,0.48L3,20.5c0,0.28 0.22,0.5 0.5,0.5l0.16,-0.03L9,18.9l6,2.1 5.64,-1.9c0.21,-0.07 0.36,-0.25 0.36,-0.48L21,3.5c0,-0.28 -0.22,-0.5 -0.5,-0.5zM15,19l-6,-2.11L9,5l6,2.11L15,19z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.14,12.94c0.04,-0.31 0.06,-0.63 0.06,-0.94c0,-0.31 -0.02,-0.63 -0.06,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.37 4.8,11.69 4.8,12s0.02,0.63 0.06,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/bottom_nav"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/bottom_nav_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,302 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Flight Dashboard -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Flight Dashboard"
android:textSize="18sp"
android:textStyle="bold" />
<com.senstools.presentation.ui.widget.ArtificialHorizonView
android:id="@+id/artificialHorizon"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginTop="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvPitch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="Pitch: 0.00¡ã" />
<TextView
android:id="@+id/tvRoll"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="Roll: 0.00¡ã" />
<TextView
android:id="@+id/tvYaw"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="Yaw: 0.00¡ã" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- GPS Data -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="GPS Data"
android:textSize="18sp"
android:textStyle="bold" />
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:columnCount="2">
<TextView android:text="Longitude:" />
<TextView android:id="@+id/tvLongitude" android:text="0.00000000" />
<TextView android:text="Latitude:" />
<TextView android:id="@+id/tvLatitude" android:text="0.00000000" />
<TextView android:text="Altitude:" />
<TextView android:id="@+id/tvAltitude" android:text="0.00 m" />
<TextView android:text="Speed:" />
<TextView android:id="@+id/tvSpeed" android:text="0.00 m/s" />
<TextView android:text="Bearing:" />
<TextView android:id="@+id/tvBearing" android:text="0.00¡ã" />
<TextView android:text="Satellites:" />
<TextView android:id="@+id/tvSatellites" android:text="0" />
</GridLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Sensor Data -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sensor Data"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Accelerometer (m/s?)" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvAccelX"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="X: 0.00" />
<TextView
android:id="@+id/tvAccelY"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Y: 0.00" />
<TextView
android:id="@+id/tvAccelZ"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Z: 0.00" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Gyroscope (¡ã/s)" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvGyroX"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="X: 0.00" />
<TextView
android:id="@+id/tvGyroY"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Y: 0.00" />
<TextView
android:id="@+id/tvGyroZ"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Z: 0.00" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Magnetometer (¦ÌT)" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvMagX"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="X: 0.00" />
<TextView
android:id="@+id/tvMagY"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Y: 0.00" />
<TextView
android:id="@+id/tvMagZ"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Z: 0.00" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Environment" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvPressure"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Alt: 0.00 m" />
<TextView
android:id="@+id/tvTemperature"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Temp: 0.00¡ãC" />
<TextView
android:id="@+id/tvHumidity"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Hum: 0.0%" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Recording Controls -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnStartRecording"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:text="Start Recording" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnStopRecording"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:enabled="false"
android:text="Stop Recording" />
</LinearLayout>
</LinearLayout>
</ScrollView>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/mapFragment"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Data Collection Settings -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Data Collection"
android:textSize="18sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="GPS Data" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchGps"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="GPS Sampling Rate (Hz)" />
<com.google.android.material.slider.Slider
android:id="@+id/sliderGpsRate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:valueFrom="1"
android:valueTo="10"
android:stepSize="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Sensor Data" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchSensor"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Sensor Sampling Rate (Hz)" />
<com.google.android.material.slider.Slider
android:id="@+id/sliderSensorRate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:valueFrom="10"
android:valueTo="100"
android:stepSize="10" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Display Settings -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Display"
android:textSize="18sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Map Display" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchMap"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- About -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="About"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="sensTools v1.0.0" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Sensor data collection and analysis tool" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvFileName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="filename.csv"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvFileSize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1.2 MB"
android:textSize="12sp"
android:textColor="@android:color/darker_gray" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="?"
android:textColor="@android:color/darker_gray" />
<TextView
android:id="@+id/tvFileDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2024-01-15 14:30:00"
android:textSize="12sp"
android:textColor="@android:color/darker_gray" />
</LinearLayout>
</LinearLayout>
<ImageButton
android:id="@+id/btnExport"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Export"
android:src="@drawable/ic_export" />
<ImageButton
android:id="@+id/btnDelete"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Delete"
android:src="@drawable/ic_delete" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/dashboardFragment"
android:icon="@drawable/ic_dashboard"
android:title="@string/nav_dashboard" />
<item
android:id="@+id/dataFragment"
android:icon="@drawable/ic_data"
android:title="@string/nav_data" />
<item
android:id="@+id/mapFragment"
android:icon="@drawable/ic_map"
android:title="@string/nav_map" />
<item
android:id="@+id/settingsFragment"
android:icon="@drawable/ic_settings"
android:title="@string/nav_settings" />
</menu>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/primary"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/primary"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/dashboardFragment">
<fragment
android:id="@+id/dashboardFragment"
android:name="com.senstools.presentation.ui.dashboard.DashboardFragment"
android:label="Dashboard"
tools:layout="@layout/fragment_dashboard" />
<fragment
android:id="@+id/dataFragment"
android:name="com.senstools.presentation.ui.data.DataFragment"
android:label="Data"
tools:layout="@layout/fragment_data" />
<fragment
android:id="@+id/mapFragment"
android:name="com.senstools.presentation.ui.map.MapFragment"
android:label="Map"
tools:layout="@layout/fragment_map" />
<fragment
android:id="@+id/settingsFragment"
android:name="com.senstools.presentation.ui.settings.SettingsFragment"
android:label="Settings"
tools:layout="@layout/fragment_settings" />
</navigation>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.sensTools" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryVariant">@color/primary_dark</item>
<item name="colorOnPrimary">@color/white</item>
<item name="colorSecondary">@color/secondary</item>
<item name="colorOnSecondary">@color/white</item>
<item name="android:statusBarColor">@color/primary_dark</item>
</style>
<color name="primary">#1976D2</color>
<color name="primary_dark">#1565C0</color>
<color name="secondary">#FF6F00</color>
<color name="white">#FFFFFF</color>
<color name="black">#000000</color>
<color name="background">#F5F5F5</color>
<color name="surface">#FFFFFF</color>
<color name="error">#B00020</color>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">sensTools</string>
<string name="nav_dashboard">Dashboard</string>
<string name="nav_data">Data</string>
<string name="nav_map">Map</string>
<string name="nav_settings">Settings</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
</full-backup-content>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
</cloud-backup>
</data-extraction-rules>
+5
View File
@@ -0,0 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.2.0' apply false
id 'org.jetbrains.kotlin.android' version '1.9.20' apply false
}
+6
View File
@@ -0,0 +1,6 @@
# Project-wide Gradle settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
android.enableJetifier=true
kotlin.code.style=official
android.nonTransitiveRClass=true
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+17
View File
@@ -0,0 +1,17 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "sensTools"
include ':app'
Binary file not shown.
Binary file not shown.