diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..e3a86b4
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+pilot
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b589d56
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..ae388c2
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..44ca2d9
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..0fc3113
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..8978d23
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..67a7214
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,74 @@
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "com.helible.pilot"
+ compileSdk = 33
+
+ defaultConfig {
+ applicationId = "com.helible.pilot"
+ minSdk = 28
+ targetSdk = 33
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.4.3"
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+
+ implementation("androidx.core:core-ktx:1.9.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
+ implementation("androidx.activity:activity-compose:1.7.2")
+ implementation(platform("androidx.compose:compose-bom:2023.03.00"))
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-graphics")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
+ implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
+ implementation("androidx.navigation:navigation-compose:2.6.0")
+ implementation("com.google.code.gson:gson:2.10.1")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.5")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+ androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/release/app-release.apk b/app/release/app-release.apk
new file mode 100644
index 0000000..9fec070
Binary files /dev/null and b/app/release/app-release.apk differ
diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json
new file mode 100644
index 0000000..850a9f6
--- /dev/null
+++ b/app/release/output-metadata.json
@@ -0,0 +1,20 @@
+{
+ "version": 3,
+ "artifactType": {
+ "type": "APK",
+ "kind": "Directory"
+ },
+ "applicationId": "com.helible.pilot",
+ "variantName": "release",
+ "elements": [
+ {
+ "type": "SINGLE",
+ "filters": [],
+ "attributes": [],
+ "versionCode": 1,
+ "versionName": "1.0",
+ "outputFile": "app-release.apk"
+ }
+ ],
+ "elementType": "File"
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/helible/pilot/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/helible/pilot/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..1493533
--- /dev/null
+++ b/app/src/androidTest/java/com/helible/pilot/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.helible.pilot
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.gogacoder.testblue", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..cd31548
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/BluetoothHelper.kt b/app/src/main/java/com/helible/pilot/BluetoothHelper.kt
new file mode 100644
index 0000000..2c358ef
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/BluetoothHelper.kt
@@ -0,0 +1,70 @@
+package com.helible.pilot
+
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothSocket
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.onCompletion
+import java.io.IOException
+import java.util.UUID
+
+sealed interface ConnectionResult {
+ object ConnectionEstablished: ConnectionResult
+ data class Error(val message: String) : ConnectionResult
+}
+
+interface BluetoothController {
+ val isConnected: StateFlow
+ val errors: SharedFlow
+
+ fun connectToDevice(device: Device?): Flow
+ fun closeConnection()
+}
+
+class AndroidBluetoothController : BluetoothController {
+
+ private val _isConnected: MutableStateFlow = MutableStateFlow(false)
+ override val isConnected: StateFlow
+ get() = _isConnected.asStateFlow()
+
+ private val _errors = MutableSharedFlow()
+ override val errors: SharedFlow
+ get() = _errors.asSharedFlow()
+
+ private var currentClientSocket: BluetoothSocket? = null
+
+ companion object {
+ const val SERVICE_UUID = "af7cc14b-cffa-4a3d-b677-01b0ff0a93d7"
+ }
+
+ @SuppressLint("MissingPermission")
+ override fun connectToDevice(device: Device?): Flow {
+ return flow {
+ currentClientSocket = device?.bluetoothDevice?.createRfcommSocketToServiceRecord(
+ UUID.fromString(SERVICE_UUID)
+ )
+ currentClientSocket?.let { socket ->
+ try {
+ socket.connect()
+ emit(ConnectionResult.ConnectionEstablished)
+ } catch (e: IOException) {
+ closeConnection()
+ emit(ConnectionResult.Error("Connection was interrupted"))
+ }
+ }
+ }.onCompletion { closeConnection() }.flowOn(Dispatchers.IO)
+ }
+
+ override fun closeConnection() {
+ currentClientSocket?.close()
+ currentClientSocket = null
+ }
+}
diff --git a/app/src/main/java/com/helible/pilot/MainActivity.kt b/app/src/main/java/com/helible/pilot/MainActivity.kt
new file mode 100644
index 0000000..4fea030
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/MainActivity.kt
@@ -0,0 +1,249 @@
+package com.helible.pilot
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.location.LocationManager
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.ManagedActivityResultLauncher
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.compose.setContent
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import com.helible.pilot.components.BluetoothScannerScreen
+import com.helible.pilot.components.PreferencesCacheImpl
+import com.helible.pilot.components.SavedPreferences
+import com.helible.pilot.ui.theme.TestblueTheme
+import java.util.concurrent.Executors
+
+@SuppressLint("MissingPermission")
+class MainActivity : ComponentActivity() {
+ // TODO: delegate part of Intent filters logic to BluetoothController
+ // TODO: move bluetooth states and stateFlow to new BluetoothViewModel
+ // TODO: replace field bluetoothDevice in Device to deviceAddress field
+ // TODO: replace some mutableStates to stateFlows
+ // TODO: share selected device via PersistentViewModel
+ // TODO: check permissions inside other classes (and throw an exception, if one of this isn't granted)
+ // TODO: add stub instead of the DevicesList, if there aren't nearby devices
+ // TODO: add Bluetooth data transfer...
+ // TODO: add text strings to resource
+ val mainViewModel: MainViewModel = MainViewModel(AndroidBluetoothController())
+
+ private val bluetoothManager: BluetoothManager by lazy {
+ getSystemService(BluetoothManager::class.java)
+ }
+ private val bluetoothAdapter: BluetoothAdapter? by lazy {
+ bluetoothManager.adapter
+ }
+ private var permissionsViewModel = PermissionDialogViewModel()
+ private lateinit var permissionLauncher: ManagedActivityResultLauncher, Map>
+
+ private val permissionsToRequest: Array by lazy {
+ if (Build.VERSION.SDK_INT <= 30) {
+ arrayOf(
+ Manifest.permission.ACCESS_FINE_LOCATION
+ )
+ } else {
+ arrayOf(
+ Manifest.permission.BLUETOOTH_SCAN,
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_ADMIN
+ )
+ }
+ }
+
+ private val locationManager: LocationManager by lazy {
+ getSystemService(LOCATION_SERVICE) as LocationManager
+ }
+ private val preferencesCache by lazy {
+ PreferencesCacheImpl(getSharedPreferences(packageName, Context.MODE_PRIVATE))
+ }
+ private val preferencesViewModel by lazy {
+ PersistentViewModel(preferencesCache)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ mainViewModel.bluetoothTurnOnState.value = bluetoothAdapter?.isEnabled
+ mainViewModel.locationTurnOnState.value =
+ locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
+
+ setContent {
+ this.permissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestMultiplePermissions(),
+ onResult = { perms ->
+ permissionsToRequest.forEach { permission ->
+ permissionsViewModel.onPermissionResult(
+ permission = permission,
+ isGranted = perms[permission] == true
+ )
+ }
+ }
+ )
+
+ val state by mainViewModel.state.collectAsState()
+
+ LaunchedEffect(key1 = state.errorMessage) {
+ state.errorMessage?.let { message ->
+ Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
+ }
+ }
+ LaunchedEffect(key1 = state) {
+ if (state.isConnected) {
+ Toast.makeText(applicationContext, "Подключение завершено", Toast.LENGTH_LONG)
+ .show()
+ }
+ }
+
+ val navController = rememberNavController()
+ TestblueTheme {
+ Surface(color = MaterialTheme.colorScheme.background) {
+ PermissionsRequest(
+ dismissCurrentDialog = { permissionsViewModel.dismissDialog() },
+ visiblePermissionDialogQueue = permissionsViewModel.visiblePermissionDialogQueue,
+ activity = this,
+ permissionLauncher = permissionLauncher
+ )
+
+ RequestHardwareFeatures(
+ activity = this,
+ turnOnLocation = Manifest.permission.ACCESS_FINE_LOCATION in permissionsToRequest,
+ bluetoothTurnOnState = mainViewModel.bluetoothTurnOnState,
+ locationTurnOnState = mainViewModel.locationTurnOnState
+ )
+
+ NavHost(navController = navController, startDestination = "scanner") {
+ composable("scanner") {
+ BluetoothScannerScreen(
+ devices = mainViewModel.devices,
+ selectedDevice = mainViewModel.selectedDevice,
+ bluetoothIsDiscoveringState = mainViewModel.isBluetoothDiscoveryRunning,
+ bluetoothAdapter = bluetoothAdapter,
+ onScreenChanged = {
+ bluetoothAdapter?.cancelDiscovery()
+ preferencesViewModel.savePreferences(
+ SavedPreferences(
+ mainViewModel.selectedDevice.value?.bluetoothDevice?.address
+ )
+ )
+ navController.navigate("flight")
+ Log.i(
+ "ScanActivity",
+ "Preferences: ${preferencesViewModel.preferences}"
+ )
+ }
+ )
+ }
+ composable("flight") {
+
+ LaunchedEffect(Unit) {
+ // TODO: refactor
+ val device: Device = mainViewModel.selectedDevice.value!!
+ mainViewModel.connectToDevice(device)
+ }
+ when {
+ state.isConnecting -> {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ CircularProgressIndicator()
+ Text(text = "Подключение...", textAlign = TextAlign.Center)
+ }
+ }
+
+ else -> {
+ Text(
+ text = "Device name: ${mainViewModel.selectedDevice.value?.bluetoothDevice?.name}",
+ modifier = Modifier.fillMaxSize(),
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ registerIntentFilters(this, receiver)
+ requestPermissions()
+ }
+
+ private val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent) {
+ receiveIntentChanges(
+ intent,
+ mainViewModel,
+ bluetoothAdapter,
+ locationManager
+ )
+ }
+ }
+
+ private fun requestPermissions() {
+ Executors.newSingleThreadExecutor().execute {
+ Handler(Looper.getMainLooper()).post {
+ permissionLauncher.launch(permissionsToRequest)
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ unregisterReceiver(receiver)
+ super.onDestroy()
+ Log.i("ScanActivity", "ACTIVITY DESTROYED")
+ bluetoothAdapter?.cancelDiscovery()
+ try {
+ unregisterReceiver(receiver)
+ } catch (e: IllegalArgumentException) {
+ Log.e(
+ "ScanActivity",
+ "Receiver wasn't registered ${e.localizedMessage}\nStackTrace: ${e.stackTrace}"
+ )
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (bluetoothAdapter?.isDiscovering != true)
+ bluetoothAdapter?.startDiscovery()
+ Log.i("ScanActivity", "ACTIVITY STARTED")
+ }
+
+ override fun onStop() {
+ super.onStop()
+ bluetoothAdapter?.cancelDiscovery()
+ mainViewModel.devices.clear()
+ mainViewModel.selectedDevice.value = null
+ Log.i("ScanActivity", "ACTIVITY STOPPED")
+ }
+}
+
+
diff --git a/app/src/main/java/com/helible/pilot/MainViewModel.kt b/app/src/main/java/com/helible/pilot/MainViewModel.kt
new file mode 100644
index 0000000..11c6ff0
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/MainViewModel.kt
@@ -0,0 +1,133 @@
+package com.helible.pilot
+
+import android.bluetooth.BluetoothDevice
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.helible.pilot.components.SavedPreferences
+import com.helible.pilot.components.SavedPreferencesCache
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+
+data class Device(
+ val bluetoothDevice: BluetoothDevice,
+ val rssi: Short,
+)
+
+data class BluetoothUiState(
+ val isConnected: Boolean = false,
+ val isConnecting: Boolean = false,
+ val errorMessage: String? = null
+)
+
+class MainViewModel(
+ private val bluetoothController: BluetoothController
+) : ViewModel() {
+ val devices: MutableList = mutableStateListOf()
+ val selectedDevice: MutableState = mutableStateOf(null)
+ val bluetoothTurnOnState: MutableState = mutableStateOf(false)
+ val locationTurnOnState: MutableState = mutableStateOf(null)
+ val isBluetoothDiscoveryRunning: MutableState = mutableStateOf(false)
+
+ private val _bluetoothState = MutableStateFlow(BluetoothUiState())
+ val state: StateFlow
+ get() = _bluetoothState.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _bluetoothState.value)
+
+ init {
+ bluetoothController.isConnected.onEach {
+ isConnected -> _bluetoothState.update { it.copy(isConnected = isConnected) }
+ }.launchIn(viewModelScope)
+ bluetoothController.errors.onEach { error ->
+ _bluetoothState.update {
+ it.copy(errorMessage = error)
+ }
+ }.launchIn(viewModelScope)
+ }
+
+ private fun Flow.listen(): Job {
+ return onEach { result ->
+ when(result) {
+ ConnectionResult.ConnectionEstablished -> {
+ _bluetoothState.update {
+ it.copy(
+ isConnected = true,
+ isConnecting = false,
+ errorMessage = null
+ )
+ }
+ }
+ is ConnectionResult.Error -> {
+ _bluetoothState.update { it.copy(
+ isConnected = false,
+ isConnecting = false,
+ errorMessage = result.message
+ ) }
+ }
+ }
+ }
+ .catch { throwable ->
+ bluetoothController.closeConnection()
+ _bluetoothState.update {
+ it.copy(
+ isConnected = false,
+ isConnecting = false
+ )
+ }
+ }
+ .launchIn(viewModelScope)
+ }
+ private var deviceConnectionJob: Job? = null
+
+ fun connectToDevice(device: Device) {
+ _bluetoothState.update {it.copy(isConnecting = true)}
+ deviceConnectionJob = bluetoothController
+ .connectToDevice(device)
+ .listen()
+ }
+
+ fun disconnectFromDevice() {
+ deviceConnectionJob?.cancel()
+ bluetoothController.closeConnection()
+ _bluetoothState.update {
+ it.copy(
+ isConnecting = false,
+ isConnected = false
+ )
+ }
+ }
+}
+
+class PermissionDialogViewModel: ViewModel() {
+ val visiblePermissionDialogQueue = mutableStateListOf()
+
+ fun dismissDialog() {
+ visiblePermissionDialogQueue.removeFirst()
+ }
+
+ fun onPermissionResult(permission: String, isGranted: Boolean) {
+ if(!isGranted && !visiblePermissionDialogQueue.contains(permission)){
+ visiblePermissionDialogQueue.add(permission)
+ }
+ }
+}
+
+class PersistentViewModel(
+ private val preferencesCache: SavedPreferencesCache,
+) : ViewModel() {
+ val preferences get() = preferencesCache.getPreferences()
+ fun savePreferences(savedPreferences: SavedPreferences) {
+ preferencesCache.savePreferences(
+ preferences = savedPreferences
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/PermissionsRequest.kt b/app/src/main/java/com/helible/pilot/PermissionsRequest.kt
new file mode 100644
index 0000000..ad22353
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/PermissionsRequest.kt
@@ -0,0 +1,70 @@
+package com.helible.pilot
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.provider.Settings
+import androidx.activity.compose.ManagedActivityResultLauncher
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import com.helible.pilot.components.BluetoothAdminPermissionTextProvider
+import com.helible.pilot.components.BluetoothConnectPermissionTextProvider
+import com.helible.pilot.components.BluetoothScanPermissionTextProvider
+import com.helible.pilot.components.LocationPermissionTextProvider
+import com.helible.pilot.components.PermissionDialog
+
+@Composable
+fun PermissionsRequest(
+ visiblePermissionDialogQueue: SnapshotStateList,
+ dismissCurrentDialog: () -> Unit,
+ activity: Activity,
+ permissionLauncher: ManagedActivityResultLauncher, Map>
+) {
+ /* Create Dialog windows, which requests all permissions */
+ visiblePermissionDialogQueue.reversed()
+ .forEach { permission ->
+ PermissionDialog(
+ permissionTextProvider = when (permission) {
+ Manifest.permission.ACCESS_FINE_LOCATION -> {
+ LocationPermissionTextProvider()
+ }
+
+ Manifest.permission.BLUETOOTH_SCAN -> {
+ BluetoothScanPermissionTextProvider()
+ }
+
+ Manifest.permission.BLUETOOTH_CONNECT -> {
+ BluetoothConnectPermissionTextProvider()
+ }
+
+ Manifest.permission.BLUETOOTH_ADMIN -> {
+ BluetoothAdminPermissionTextProvider()
+ }
+
+ else -> return@forEach
+ },
+ isPermanentDeclined = !activity.shouldShowRequestPermissionRationale(permission),
+ onDismiss = {
+ if (activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED)
+ dismissCurrentDialog()
+ },
+ onOkClick = {
+ dismissCurrentDialog()
+ permissionLauncher.launch(arrayOf(permission))
+ },
+ onContinueClick = {
+ if (activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED)
+ dismissCurrentDialog()
+ },
+ onGoToAppSettingsClick = {
+ val intent = Intent(
+ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ Uri.fromParts("package", activity.packageName, null)
+ )
+ activity.startActivity(intent)
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/ReceiveIntentChanges.kt b/app/src/main/java/com/helible/pilot/ReceiveIntentChanges.kt
new file mode 100644
index 0000000..5ca5702
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/ReceiveIntentChanges.kt
@@ -0,0 +1,86 @@
+package com.helible.pilot
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.content.BroadcastReceiver
+import android.content.Intent
+import android.content.IntentFilter
+import android.location.LocationManager
+import android.os.Build
+import android.util.Log
+
+
+fun registerIntentFilters(activity: Activity, receiver: BroadcastReceiver) {
+ activity.registerReceiver(receiver, IntentFilter(BluetoothDevice.ACTION_FOUND))
+ activity.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
+ activity.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_STARTED))
+ activity.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED))
+ if (Build.VERSION.SDK_INT <= 30)
+ activity.registerReceiver(receiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION))
+}
+
+@SuppressLint("MissingPermission")
+fun receiveIntentChanges(
+ intent: Intent,
+ mainViewModel: MainViewModel,
+ bluetoothAdapter: BluetoothAdapter?,
+ locationManager: LocationManager,
+) {
+ when (intent.action) {
+ BluetoothDevice.ACTION_FOUND -> {
+ val device = if (Build.VERSION.SDK_INT >= 33) {
+ intent.getParcelableExtra(
+ BluetoothDevice.EXTRA_DEVICE,
+ BluetoothDevice::class.java
+ )
+ } else {
+ @Suppress("DEPRECATION") intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
+ }
+
+ val rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE)
+ if (device?.name != null)
+ mainViewModel.devices.add(Device(device, rssi))
+ Log.i(
+ "ScanActivity",
+ "Found new device: ${device?.name} ${device?.address} $rssi"
+ )
+ }
+
+ BluetoothAdapter.ACTION_STATE_CHANGED -> {
+ val state: Int = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)
+ Log.i("ScanActvity", "Bluetooth state: $state")
+ when (state) {
+ BluetoothAdapter.STATE_ON -> {
+ Log.i("ScanActvity", "Bluetooth turned on")
+ mainViewModel.bluetoothTurnOnState.value = true
+ mainViewModel.devices.clear()
+ bluetoothAdapter?.startDiscovery()
+ }
+
+ BluetoothAdapter.STATE_OFF -> {
+ Log.i("ScanActvity", "Bluetooth turned off")
+ mainViewModel.bluetoothTurnOnState.value = false
+ }
+ }
+ }
+ LocationManager.PROVIDERS_CHANGED_ACTION -> {
+ mainViewModel.locationTurnOnState.value = locationManager.isLocationEnabled
+ if (mainViewModel.locationTurnOnState.value == true) {
+ Log.i("ScanActivity", "LOCATION IS ON")
+ } else if (mainViewModel.locationTurnOnState.value == false)
+ Log.i("ScanActivity", "LOCATION IS OFF")
+ }
+
+ BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
+ mainViewModel.isBluetoothDiscoveryRunning.value = false
+ Log.i("ScanActivity", "DISCOVERY FINISHED")
+ }
+
+ BluetoothAdapter.ACTION_DISCOVERY_STARTED -> {
+ mainViewModel.isBluetoothDiscoveryRunning.value = true
+ Log.i("ScanActivity", "DISCOVERY STARTED")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/RequestHardwareFeatures.kt b/app/src/main/java/com/helible/pilot/RequestHardwareFeatures.kt
new file mode 100644
index 0000000..68dc3d2
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/RequestHardwareFeatures.kt
@@ -0,0 +1,46 @@
+package com.helible.pilot
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.bluetooth.BluetoothAdapter
+import android.content.Intent
+import android.provider.Settings
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import com.helible.pilot.components.RequiredHardwareFeatures
+
+@SuppressLint("MissingPermission")
+@Composable
+fun RequestHardwareFeatures(
+ activity: Activity,
+ turnOnLocation: Boolean,
+ bluetoothTurnOnState: MutableState,
+ locationTurnOnState: MutableState
+)
+{
+ RequiredHardwareFeatures(
+ title = "Включите Bluetooth",
+ description = "Для работы приложения требуется Bluetooth",
+ confirmButtonText = "Включить Bluetooth",
+ featureState = bluetoothTurnOnState,
+ requestFeature = {
+ val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
+ activity.startActivity(intent)
+ },
+ onDismissRequest = {}
+ )
+
+ if (turnOnLocation) {
+ RequiredHardwareFeatures(
+ title = "Пожалуйста, включите геолокацию",
+ description = "Для работы с Bluetooth на устройствах с Android 11 и более ранних версиях, " +
+ "требуется геолокация.",
+ confirmButtonText = "Включить геолокацию",
+ featureState = locationTurnOnState,
+ requestFeature = {
+ val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
+ activity.startActivity(intent)
+ }
+ ) {}
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/components/BluetoothScannerScreen.kt b/app/src/main/java/com/helible/pilot/components/BluetoothScannerScreen.kt
new file mode 100644
index 0000000..3a2cb4f
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/components/BluetoothScannerScreen.kt
@@ -0,0 +1,118 @@
+package com.helible.pilot.components
+
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothAdapter
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material3.Button
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.FilledIconToggleButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.ConstraintLayout
+import androidx.constraintlayout.compose.Dimension
+import com.helible.pilot.Device
+
+
+@SuppressLint("MissingPermission")
+@Composable
+fun BluetoothScannerScreen(
+ devices: MutableList,
+ selectedDevice: MutableState,
+ bluetoothIsDiscoveringState: MutableState,
+ bluetoothAdapter: BluetoothAdapter?,
+ onScreenChanged: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ modifier = modifier,
+ ) {
+ ConstraintLayout(modifier = Modifier.fillMaxSize()) {
+ val (title, devicesList, controls) = createRefs()
+
+ Title(
+ text = "Поиск устройств",
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 10.dp)
+ .constrainAs(title) {}
+ )
+
+ DiscoveredDevicesList(
+ devices = devices,
+ selectedDevice = selectedDevice,
+ modifier = Modifier
+ .constrainAs(devicesList) {
+ top.linkTo(title.bottom)
+ bottom.linkTo(controls.top)
+ height = Dimension.fillToConstraints
+ }
+ )
+
+ if (devices.isEmpty() && bluetoothIsDiscoveringState.value) {
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+ }
+
+ Row(
+ modifier = Modifier
+ .padding(5.dp)
+ .constrainAs(controls) {
+ bottom.linkTo(parent.bottom)
+ width = Dimension.matchParent
+ height = Dimension.fillToConstraints
+ },
+ horizontalArrangement = Arrangement.Center
+ ) {
+ FilledIconToggleButton(
+ checked = bluetoothIsDiscoveringState.value,
+ onCheckedChange = {
+ selectedDevice.value = null
+ if (bluetoothIsDiscoveringState.value)
+ bluetoothAdapter?.cancelDiscovery()
+ else {
+ devices.clear()
+ bluetoothAdapter?.startDiscovery()
+ }
+ }, modifier = Modifier
+ .align(Alignment.Bottom)
+ .padding(5.dp)
+ ) {
+ Icon(
+ if (bluetoothIsDiscoveringState.value) Icons.Filled.Close
+ else Icons.Filled.Refresh,
+ contentDescription = null
+ )
+ }
+ Button(
+ onClick = {
+ onScreenChanged()
+ },
+ modifier = Modifier
+ .align(Alignment.Bottom)
+ .padding(5.dp),
+ enabled = selectedDevice.value != null,
+ ) {
+ Text(text = "Далее")
+ }
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/components/DeviceItem.kt b/app/src/main/java/com/helible/pilot/components/DeviceItem.kt
new file mode 100644
index 0000000..8bd8dbd
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/components/DeviceItem.kt
@@ -0,0 +1,71 @@
+package com.helible.pilot.components
+
+import android.annotation.SuppressLint
+import android.widget.TextView
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.text.HtmlCompat
+import com.helible.pilot.Device
+import com.helible.pilot.R
+
+@SuppressLint("MissingPermission")
+@Composable
+fun DeviceItem(deviceInfo: Device, selectedDevice: MutableState, modifier: Modifier) {
+ ElevatedCard(
+ modifier=modifier.clickable {
+ selectedDevice.value = deviceInfo
+ },
+ colors = CardDefaults.elevatedCardColors(containerColor = if (deviceInfo.bluetoothDevice == selectedDevice.value?.bluetoothDevice)
+ MaterialTheme.colorScheme.secondaryContainer
+ else MaterialTheme.colorScheme.surface
+ )
+ ) {
+ Row(modifier=Modifier.padding(8.dp)) {
+ Column(verticalArrangement = Arrangement.Center) {
+ Text(text=deviceInfo.bluetoothDevice.name, fontWeight = FontWeight.Bold, softWrap = true)
+ AndroidView(factory = { context ->
+ TextView(context).apply {
+ // Using old TextView for text formatting
+ text = HtmlCompat.fromHtml("MAC: ${deviceInfo.bluetoothDevice.address}", HtmlCompat.FROM_HTML_MODE_LEGACY)
+ }
+ })
+ }
+ Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.fillMaxSize()) {
+ Icon(
+ painterResource(id = getSignalIconForRssiValue(deviceInfo.rssi)),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(10.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+ }
+}
+
+fun getSignalIconForRssiValue(rssi: Short): Int{
+ if (rssi >= -80) return R.drawable.signal_icon4
+ else if (rssi >= -90) return R.drawable.signal_icon3
+ else if (rssi >= -100) return R.drawable.signal_icon2
+ return R.drawable.signal_icon1
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/components/DiscoveredDevicesList.kt b/app/src/main/java/com/helible/pilot/components/DiscoveredDevicesList.kt
new file mode 100644
index 0000000..263c5fb
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/components/DiscoveredDevicesList.kt
@@ -0,0 +1,28 @@
+package com.helible.pilot.components
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.helible.pilot.Device
+
+@Composable
+fun DiscoveredDevicesList(devices: MutableList, selectedDevice: MutableState, modifier: Modifier = Modifier) {
+ LazyColumn(modifier = modifier) {
+ items(devices) { device ->
+ DeviceItem(
+ deviceInfo = device,
+ selectedDevice = selectedDevice,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ 5.dp
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/components/FeatureRequestDialog.kt b/app/src/main/java/com/helible/pilot/components/FeatureRequestDialog.kt
new file mode 100644
index 0000000..3fa2bcd
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/components/FeatureRequestDialog.kt
@@ -0,0 +1,38 @@
+package com.helible.pilot.components
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Divider
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Modifier
+
+@Composable
+fun RequiredHardwareFeatures(
+ title: String,
+ description: String,
+ confirmButtonText: String,
+ featureState: MutableState,
+ requestFeature: () -> Unit,
+ onDismissRequest: () -> Unit
+) {
+ if (featureState.value == false || featureState.value == null) {
+ AlertDialog(
+ confirmButton = {
+ Divider()
+ TextButton(onClick = requestFeature, modifier = Modifier.fillMaxWidth()) {
+ Text(text=confirmButtonText)
+ }
+ },
+ onDismissRequest = onDismissRequest,
+ text = {
+ Text(
+ text = description
+ )
+ },
+ title = { Text(text = title)}
+ )
+ }
+}
diff --git a/app/src/main/java/com/helible/pilot/components/PermissionDialog.kt b/app/src/main/java/com/helible/pilot/components/PermissionDialog.kt
new file mode 100644
index 0000000..dce70db
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/components/PermissionDialog.kt
@@ -0,0 +1,130 @@
+package com.helible.pilot.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Divider
+import androidx.compose.material3.Text
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun PermissionDialog(
+ permissionTextProvider: PermissionTextProvider,
+ isPermanentDeclined: Boolean,
+ onDismiss: () -> Unit,
+ onOkClick: () -> Unit,
+ onContinueClick: () -> Unit,
+ onGoToAppSettingsClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text(text = "Требуется разрешение") },
+ confirmButton = {
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Divider()
+ Text(
+ text = if (isPermanentDeclined) {
+ "Выдать разрешение"
+ } else {
+ "OK"
+ },
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ if (isPermanentDeclined) {
+ onGoToAppSettingsClick()
+ } else {
+ onOkClick()
+ }
+ }
+ .padding(16.dp)
+ )
+ }
+ },
+ dismissButton = {if(isPermanentDeclined)
+ Box(modifier=Modifier.fillMaxWidth()){
+ Divider()
+ Text(
+ text = "Снова проверить наличие разрешения",
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(paddingValues = PaddingValues(top=10.dp))
+ .clickable {onContinueClick()},
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center
+ )}
+ else
+ Unit},
+ text = {
+ Text(text = permissionTextProvider.getDescription(
+ isPermanentDeclined = isPermanentDeclined
+ ))
+ },
+ modifier = modifier
+ )
+}
+
+interface PermissionTextProvider {
+ fun getDescription(isPermanentDeclined: Boolean): String
+}
+
+class LocationPermissionTextProvider : PermissionTextProvider {
+ override fun getDescription(isPermanentDeclined: Boolean): String {
+ return if (isPermanentDeclined){
+ "Похоже вы навсегда запретили приложению доступ к геолокации. " +
+ "Вы можете зайти в настройки, чтобы выдать это разрешение."
+ } else {
+ "Приложению необходимо разрешение для определения местоположения " +
+ "для работы с Bluetooth на устройствах с Android 11 и ниже."
+ }
+ }
+}
+
+class BluetoothScanPermissionTextProvider : PermissionTextProvider {
+ override fun getDescription(isPermanentDeclined: Boolean): String {
+ return if (isPermanentDeclined){
+ "Похоже вы навсегда запретили приложению доступ к сканированию по Bluetooth. " +
+ "Вы можете зайти в настройки, чтобы выдать это разрешение."
+ } else {
+ "Приложению необходимо разрешение для к сканированию по Bluetooth " +
+ "для работы с Bluetooth на устройствах с Android 11 и ниже"
+ }
+ }
+}
+
+class BluetoothConnectPermissionTextProvider : PermissionTextProvider {
+ override fun getDescription(isPermanentDeclined: Boolean): String {
+ return if (isPermanentDeclined){
+ "Похоже вы навсегда запретили приложению доступ к подключению по Bluetooth." +
+ "Вы можете зайти в настройки, чтобы выдать это разрешение."
+ } else {
+ "Приложению необходимо разрешение для к подключению по Bluetooth." +
+ "для работы с Bluetooth на устройствах с Android 11 и ниже"
+ }
+ }
+}
+
+class BluetoothAdminPermissionTextProvider : PermissionTextProvider {
+ override fun getDescription(isPermanentDeclined: Boolean): String {
+ return if (isPermanentDeclined){
+ "Похоже вы навсегда запретили приложению доступ к управлению настройками Bluetooth. " +
+ "Вы можете зайти в настройки, чтобы выдать это разрешение."
+ } else {
+ "Приложению необходимо разрешение для к управлению настройками Bluetooth " +
+ "для работы с Bluetooth на устройствах с Android 11 и ниже"
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/components/SavedPreferences.kt b/app/src/main/java/com/helible/pilot/components/SavedPreferences.kt
new file mode 100644
index 0000000..b37bb3e
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/components/SavedPreferences.kt
@@ -0,0 +1,37 @@
+package com.helible.pilot.components
+
+import android.content.SharedPreferences
+import android.os.Parcelable
+import com.google.gson.Gson
+import kotlinx.parcelize.Parcelize
+
+
+@Parcelize
+data class SavedPreferences(
+ val deviceAddress: String?
+): Parcelable
+
+interface SavedPreferencesCache {
+ fun getPreferences(): SavedPreferences?
+ fun savePreferences(preferences: SavedPreferences)
+ fun clearPreferences()
+}
+
+class PreferencesCacheImpl(private val sharedPreferences: SharedPreferences) : SavedPreferencesCache {
+ override fun getPreferences(): SavedPreferences? {
+ val json = sharedPreferences.getString("preferences", null) ?: return null
+ return Gson().fromJson(json, SavedPreferences::class.java)
+ }
+ override fun savePreferences(preferences: SavedPreferences)
+ {
+ sharedPreferences.edit()
+ .putString("preferences", Gson().toJson(preferences))
+ .apply()
+
+ }
+
+ override fun clearPreferences()
+ {
+ sharedPreferences.edit().remove("preferences").apply()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/components/Title.kt b/app/src/main/java/com/helible/pilot/components/Title.kt
new file mode 100644
index 0000000..714fa00
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/components/Title.kt
@@ -0,0 +1,19 @@
+package com.helible.pilot.components
+
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun Title(text: String, modifier: Modifier = Modifier) {
+ Text(
+ text = text,
+ textAlign = TextAlign.Center,
+ modifier = modifier,
+ fontSize = 23.sp,
+ fontWeight = FontWeight.Bold
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/ui/theme/Color.kt b/app/src/main/java/com/helible/pilot/ui/theme/Color.kt
new file mode 100644
index 0000000..075fcba
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.helible.pilot.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/ui/theme/Theme.kt b/app/src/main/java/com/helible/pilot/ui/theme/Theme.kt
new file mode 100644
index 0000000..6ad91a4
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/ui/theme/Theme.kt
@@ -0,0 +1,70 @@
+package com.helible.pilot.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun TestblueTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = colorScheme.primary.toArgb()
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
+ }
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/helible/pilot/ui/theme/Type.kt b/app/src/main/java/com/helible/pilot/ui/theme/Type.kt
new file mode 100644
index 0000000..5c31011
--- /dev/null
+++ b/app/src/main/java/com/helible/pilot/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.helible.pilot.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_android_black_24dp.xml b/app/src/main/res/drawable/ic_android_black_24dp.xml
new file mode 100644
index 0000000..fe51230
--- /dev/null
+++ b/app/src/main/res/drawable/ic_android_black_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/signal_icon1.xml b/app/src/main/res/drawable/signal_icon1.xml
new file mode 100644
index 0000000..12cd878
--- /dev/null
+++ b/app/src/main/res/drawable/signal_icon1.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/signal_icon2.xml b/app/src/main/res/drawable/signal_icon2.xml
new file mode 100644
index 0000000..7152a69
--- /dev/null
+++ b/app/src/main/res/drawable/signal_icon2.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/signal_icon3.xml b/app/src/main/res/drawable/signal_icon3.xml
new file mode 100644
index 0000000..b7dd77d
--- /dev/null
+++ b/app/src/main/res/drawable/signal_icon3.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/signal_icon4.xml b/app/src/main/res/drawable/signal_icon4.xml
new file mode 100644
index 0000000..932ac4b
--- /dev/null
+++ b/app/src/main/res/drawable/signal_icon4.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2d01e9b
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Digital Pilot
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..21ebef7
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/helible/pilot/ExampleUnitTest.kt b/app/src/test/java/com/helible/pilot/ExampleUnitTest.kt
new file mode 100644
index 0000000..a9cc21c
--- /dev/null
+++ b/app/src/test/java/com/helible/pilot/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.helible.pilot
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..3b9bab8
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,6 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id("com.android.application") version "8.1.0" apply false
+ id("org.jetbrains.kotlin.android") version "1.8.10" apply false
+ id("org.jetbrains.kotlin.jvm") version "1.8.10" apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..3c5031e
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..99bd28f
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sun Aug 13 15:00:54 KRAT 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..fb3fc77
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,17 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "pilot"
+include(":app")