First commit

This commit is contained in:
2023-08-19 22:04:05 +07:00
parent 833f8a99c1
commit a956bc3564
61 changed files with 2163 additions and 0 deletions

View File

@@ -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)
}
}

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30"/>
<!-- Request Bluetooth permissions for API level 31+ -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:minSdkVersion="31"
android:usesPermissionFlags="neverForLocation" tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" android:minSdkVersion="31"
android:usesPermissionFlags="neverForLocation" tools:targetApi="s" />
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Testblue"
tools:targetApi="31">
<activity
android:name="com.helible.pilot.MainActivity"
android:exported="true"
android:theme="@style/Theme.Testblue">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -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<Boolean>
val errors: SharedFlow<String>
fun connectToDevice(device: Device?): Flow<ConnectionResult>
fun closeConnection()
}
class AndroidBluetoothController : BluetoothController {
private val _isConnected: MutableStateFlow<Boolean> = MutableStateFlow(false)
override val isConnected: StateFlow<Boolean>
get() = _isConnected.asStateFlow()
private val _errors = MutableSharedFlow<String>()
override val errors: SharedFlow<String>
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<ConnectionResult> {
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
}
}

View File

@@ -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<Array<String>, Map<String, Boolean>>
private val permissionsToRequest: Array<String> 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")
}
}

View File

@@ -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<Device> = mutableStateListOf()
val selectedDevice: MutableState<Device?> = mutableStateOf(null)
val bluetoothTurnOnState: MutableState<Boolean?> = mutableStateOf(false)
val locationTurnOnState: MutableState<Boolean?> = mutableStateOf(null)
val isBluetoothDiscoveryRunning: MutableState<Boolean> = mutableStateOf(false)
private val _bluetoothState = MutableStateFlow(BluetoothUiState())
val state: StateFlow<BluetoothUiState>
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<ConnectionResult>.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<String>()
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
)
}
}

View File

@@ -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<String>,
dismissCurrentDialog: () -> Unit,
activity: Activity,
permissionLauncher: ManagedActivityResultLauncher<Array<String>, Map<String, Boolean>>
) {
/* 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)
}
)
}
}

View File

@@ -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")
}
}
}

View File

@@ -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<Boolean?>,
locationTurnOnState: MutableState<Boolean?>
)
{
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)
}
) {}
}
}

View File

@@ -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<Device>,
selectedDevice: MutableState<Device?>,
bluetoothIsDiscoveringState: MutableState<Boolean>,
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 = "Далее")
}
}
}
}
}

View File

@@ -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<Device?>, 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("<b>MAC</b>: ${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
}

View File

@@ -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<Device>, selectedDevice: MutableState<Device?>, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
items(devices) { device ->
DeviceItem(
deviceInfo = device,
selectedDevice = selectedDevice,
modifier = Modifier
.fillMaxWidth()
.padding(
5.dp
)
)
}
}
}

View File

@@ -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<Boolean?>,
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)}
)
}
}

View File

@@ -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 и ниже"
}
}
}

View File

@@ -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()
}
}

View File

@@ -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
)
}

View File

@@ -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)

View File

@@ -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
)
}

View File

@@ -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
)
*/
)

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"/>
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="500" android:viewportHeight="500">
<path
android:fillColor="?android:colorPrimary"
android:pathData="M 15 340 L 80 340 Q 95 340 95 355 L 95 485 Q 95 500 80 500 L 15 500 Q 0 500 0 485 L 0 355 Q 0 340 15 340 Z" />
<path
android:fillColor="?android:colorControlHighlight"
android:pathData="M 150 220 L 215 220 Q 230 220 230 235 L 230 485 Q 230 500 215 500 L 150 500 Q 135 500 135 485 L 135 235 Q 135 220 150 220 Z" />
<path
android:fillColor="?android:colorControlHighlight"
android:pathData="M 285 120 L 350 120 Q 365 120 365 135 L 365 485 Q 365 500 350 500 L 285 500 Q 270 500 270 485 L 270 135 Q 270 120 285 120 Z" />
<path
android:fillColor="?android:colorControlHighlight"
android:pathData="M 420 0 L 485 0 Q 500 0 500 15 L 500 485 Q 500 500 485 500 L 420 500 Q 405 500 405 485 L 405 15 Q 405 0 420 0 Z" />
</vector>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="500"
android:viewportHeight="500">
<path
android:fillColor="?android:colorPrimary"
android:pathData="M 15 340 L 80 340 Q 95 340 95 355 L 95 485 Q 95 500 80 500 L 15 500 Q 0 500 0 485 L 0 355 Q 0 340 15 340 Z" />
<path
android:fillColor="?android:colorPrimary"
android:pathData="M 150 220 L 215 220 Q 230 220 230 235 L 230 485 Q 230 500 215 500 L 150 500 Q 135 500 135 485 L 135 235 Q 135 220 150 220 Z" />
<path
android:fillColor="?android:colorControlHighlight"
android:pathData="M 285 120 L 350 120 Q 365 120 365 135 L 365 485 Q 365 500 350 500 L 285 500 Q 270 500 270 485 L 270 135 Q 270 120 285 120 Z" />
<path
android:fillColor="?android:colorControlHighlight"
android:pathData="M 420 0 L 485 0 Q 500 0 500 15 L 500 485 Q 500 500 485 500 L 420 500 Q 405 500 405 485 L 405 15 Q 405 0 420 0 Z" />
</vector>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="500"
android:viewportHeight="500">
<path
android:fillColor="?android:colorPrimary"
android:pathData="M 15 340 L 80 340 Q 95 340 95 355 L 95 485 Q 95 500 80 500 L 15 500 Q 0 500 0 485 L 0 355 Q 0 340 15 340 Z" />
<path
android:fillColor="?android:colorPrimary"
android:pathData="M 150 220 L 215 220 Q 230 220 230 235 L 230 485 Q 230 500 215 500 L 150 500 Q 135 500 135 485 L 135 235 Q 135 220 150 220 Z" />
<path
android:fillColor="?android:colorPrimary"
android:pathData="M 285 120 L 350 120 Q 365 120 365 135 L 365 485 Q 365 500 350 500 L 285 500 Q 270 500 270 485 L 270 135 Q 270 120 285 120 Z" />
<path
android:fillColor="?android:colorControlHighlight"
android:pathData="M 420 0 L 485 0 Q 500 0 500 15 L 500 485 Q 500 500 485 500 L 420 500 Q 405 500 405 485 L 405 15 Q 405 0 420 0 Z" />
</vector>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="500"
android:viewportHeight="500">
<path
android:fillColor="?android:colorPrimary"
android:pathData="M 15 340 L 80 340 Q 95 340 95 355 L 95 485 Q 95 500 80 500 L 15 500 Q 0 500 0 485 L 0 355 Q 0 340 15 340 Z" />
<path
android:fillColor="?android:colorPrimary"
android:pathData="M 150 220 L 215 220 Q 230 220 230 235 L 230 485 Q 230 500 215 500 L 150 500 Q 135 500 135 485 L 135 235 Q 135 220 150 220 Z" />
<path
android:fillColor="?android:colorPrimary"
android:pathData="M 285 120 L 350 120 Q 365 120 365 135 L 365 485 Q 365 500 350 500 L 285 500 Q 270 500 270 485 L 270 135 Q 270 120 285 120 Z" />
<path
android:fillColor="?android:colorPrimary"
android:pathData="M 420 0 L 485 0 Q 500 0 500 15 L 500 485 Q 500 500 485 500 L 420 500 Q 405 500 405 485 L 405 15 Q 405 0 420 0 Z" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Digital Pilot</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Testblue" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -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)
}
}