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:
@@ -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'
|
||||
}
|
||||
Vendored
+9
@@ -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 { *; }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+87
@@ -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
|
||||
}
|
||||
}
|
||||
+184
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
Reference in New Issue
Block a user