feat: add api configuration settings

This commit is contained in:
Stefan Zollinger
2024-04-16 18:22:05 +02:00
parent 803ff2e41a
commit 83b120b1ce
9 changed files with 231 additions and 22 deletions

View File

@@ -64,4 +64,5 @@ dependencies {
implementation group: 'commons-codec', name: 'commons-codec', version: '1.16.0' implementation group: 'commons-codec', name: 'commons-codec', version: '1.16.0'
implementation "com.android.volley:volley:1.2.1" implementation "com.android.volley:volley:1.2.1"
implementation "androidx.datastore:datastore-preferences:1.0.0"
} }

View File

@@ -212,8 +212,6 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build() .build()
private val scanFilters = listOf( ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(BuildConfig.SERVICE_UUID)).build())
private val bleScanner by lazy { private val bleScanner by lazy {
bleAdapter.bluetoothLeScanner bleAdapter.bluetoothLeScanner
} }
@@ -353,6 +351,9 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
} }
fun discoverServices(device: BluetoothDevice) { fun discoverServices(device: BluetoothDevice) {
if(operationQueue.any { it.device.address === device.address && it is DiscoverServicesRequest }) {
return
}
enqueueOperation(DiscoverServicesRequest(device)) enqueueOperation(DiscoverServicesRequest(device))
} }
@@ -377,6 +378,9 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
} }
fun readRemoteRssi(device: BluetoothDevice) { fun readRemoteRssi(device: BluetoothDevice) {
if(operationQueue.any { it.device.address === device.address && it is ReadRemoteRssi }) {
return
}
enqueueOperation(ReadRemoteRssi(device)) enqueueOperation(ReadRemoteRssi(device))
} }

View File

@@ -1,5 +1,6 @@
package com.logitech.vc.kirbytest package com.logitech.vc.kirbytest
import SettingsRepository
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothDevice.BOND_BONDED import android.bluetooth.BluetoothDevice.BOND_BONDED
@@ -38,9 +39,11 @@ class KirbyDevice(
private val connectionManager: ConnectionManager, private val connectionManager: ConnectionManager,
private val bleDevice: BluetoothDevice, private val bleDevice: BluetoothDevice,
private val loggerDb: LoggerContract.LoggerDb, private val loggerDb: LoggerContract.LoggerDb,
private val settingsRepository: SettingsRepository,
private val onStateChange: (device: KirbyDevice) -> Unit, private val onStateChange: (device: KirbyDevice) -> Unit,
) : BleListener(bleDevice.address), DeviceListEntry { ) : BleListener(bleDevice.address), DeviceListEntry {
private val queue: RequestQueue = Volley.newRequestQueue(context) private val queue: RequestQueue = Volley.newRequestQueue(context)
@@ -164,10 +167,11 @@ class KirbyDevice(
} }
private fun publishMeasurement(payload: String) { private fun publishMeasurement(payload: String) {
val accessKey = BuildConfig.API_KEY val settings = settingsRepository.readSettings()
val url = BuildConfig.API_BASE_URL val accessKey = settings.apiKey
val url = settings.apiUrl
if(url.isEmpty()) { if(url.isEmpty() || accessKey.isEmpty()) {
return return
} }

View File

@@ -1,5 +1,6 @@
package com.logitech.vc.kirbytest package com.logitech.vc.kirbytest
import SettingsRepository
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
@@ -15,7 +16,6 @@ import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@@ -23,7 +23,6 @@ import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -38,6 +37,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import settingsDialog
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Timer import java.util.Timer
@@ -69,6 +69,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var loggerDb: LoggerContract.LoggerDb private lateinit var loggerDb: LoggerContract.LoggerDb
private lateinit var createFileLauncher: ActivityResultLauncher<String> private lateinit var createFileLauncher: ActivityResultLauncher<String>
private val bondedDevices = HashSet<String>() private val bondedDevices = HashSet<String>()
private lateinit var settings: SettingsRepository
private val bluetoothAdapter: BluetoothAdapter by lazy { private val bluetoothAdapter: BluetoothAdapter by lazy {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
@@ -93,7 +94,7 @@ class MainActivity : AppCompatActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
settings = SettingsRepository(applicationContext)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@@ -136,23 +137,30 @@ class MainActivity : AppCompatActivity() {
} }
} }
if(hasRequiredRuntimePermissions()) { if (hasRequiredRuntimePermissions()) {
connectionManager.startScan() connectionManager.startScan()
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
delay(5000L) delay(5000L)
connectionManager.stopScan() connectionManager.stopScan()
}
} }
} }
}
Timer().schedule(object : TimerTask() { Timer().schedule(object : TimerTask() {
override fun run() { override fun run() {
loggerDb.exportToMultipleCSV() loggerDb.exportToMultipleCSV()
} }
}, 1000, 10000) }, 1000, 10000)
lifecycleScope.launch {
settings.loadSettings()
}
} }
private fun toggleScanning(): Unit { private fun toggleScanning(): Unit {
@@ -239,7 +247,8 @@ class MainActivity : AppCompatActivity() {
Log.i("KirbyDevice", "Connecting to " + result.device.address) Log.i("KirbyDevice", "Connecting to " + result.device.address)
connectionManager.connect(result.device) connectionManager.connect(result.device)
connectionManager.readRemoteRssi(result.device)
//connectionManager.readRemoteRssi(result.device)
connectionManager.discoverServices(result.device) connectionManager.discoverServices(result.device)
} }
@@ -253,12 +262,13 @@ class MainActivity : AppCompatActivity() {
bleDevice: BluetoothDevice bleDevice: BluetoothDevice
): KirbyDevice { ): KirbyDevice {
val device = KirbyDevice(this.applicationContext, connectionManager, bleDevice, loggerDb) { val device =
val i = kirbyDevices.indexOfFirst { d -> d === it } KirbyDevice(this.applicationContext, connectionManager, bleDevice, loggerDb, settings) {
runOnUiThread { val i = kirbyDevices.indexOfFirst { d -> d === it }
deviceListAdapter.notifyItemChanged(i) runOnUiThread {
deviceListAdapter.notifyItemChanged(i)
}
} }
}
Log.i("MainActivity", bleDevice.address) Log.i("MainActivity", bleDevice.address)
connectionManager.register(device) connectionManager.register(device)
@@ -303,6 +313,12 @@ class MainActivity : AppCompatActivity() {
return true return true
} }
R.id.action_settings -> {
val settingsDialog = settingsDialog(this, settings)
settingsDialog.show()
return true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }

View File

@@ -0,0 +1,117 @@
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import android.content.Context
import android.content.DialogInterface
import android.util.Log
import android.view.LayoutInflater
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.datastore.preferences.preferencesDataStore
import com.logitech.vc.kirbytest.BuildConfig
import com.logitech.vc.kirbytest.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import java.net.URL
val Context.dataStore by preferencesDataStore(name = "app_settings")
class SettingsRepository(context: Context) {
private val dataStore = context.dataStore
private val settings = Settings(apiUrl = BuildConfig.API_BASE_URL, apiKey = BuildConfig.API_KEY)
private val coroutineScope = CoroutineScope(Dispatchers.IO)
companion object {
val API_URL = stringPreferencesKey("api_url")
val API_KEY = stringPreferencesKey("api_key")
}
@OptIn(DelicateCoroutinesApi::class)
fun saveSettings(apiUrl: String, apiKey: String) {
settings.apiKey = apiKey
settings.apiUrl = apiUrl
coroutineScope.launch(Dispatchers.Main) {
dataStore.edit { preferences ->
preferences[API_URL] = apiUrl
preferences[API_KEY] = apiKey
}
}
}
fun readSettings(): Settings {
return settings
}
private val settingsFlow: Flow<Settings> = dataStore.data.map {
Settings(
apiUrl = it[API_URL] ?: BuildConfig.API_BASE_URL,
apiKey = it[API_KEY] ?: BuildConfig.API_KEY
)
}
suspend fun loadSettings() {
val result = settingsFlow.firstOrNull()
if (result != null) {
settings.apiKey = result.apiKey
settings.apiUrl = result.apiUrl
}
}
}
data class Settings(var apiUrl: String, var apiKey: String)
fun settingsDialog(context: Context, settingsRepo: SettingsRepository): AlertDialog {
val layoutInflater = LayoutInflater.from(context)
val root = layoutInflater.inflate(R.layout.settings_dialog, null)
val urlField = root.findViewById<EditText>(R.id.apiUrl)
val keyField = root.findViewById<EditText>(R.id.apiKey)
val settings = settingsRepo.readSettings()
urlField.setText(settings.apiUrl)
keyField.setText(settings.apiKey)
return androidx.appcompat.app.AlertDialog.Builder(context)
.setTitle(R.string.settings)
.setView(root)
.setPositiveButton(R.string.save) { dialog: DialogInterface?, whichButton: Int ->
Log.d("SettingsDialog", "save settings")
val url = urlField.text.toString()
val key = keyField.text.toString()
if (isFullPath(url) || url.isEmpty()) {
settingsRepo.saveSettings(url, key)
}
}
.setNegativeButton(R.string.cancel) { dialog: DialogInterface?, whichButton: Int ->
//Do something
Log.d("SettingsDialog", "cancel settings")
}
.create()
}
fun isFullPath(potentialUrl: String): Boolean {
try {
URL(potentialUrl).toURI()
return true
} catch (e: Exception) {
e.printStackTrace()
}
return false
}

View File

@@ -44,7 +44,7 @@
android:id="@+id/fab" android:id="@+id/fab"
android:layout_width="150dp" android:layout_width="150dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:text='Start Scan' android:text='@string/start_scan'
app:icon="@drawable/action_icon_scan" app:icon="@drawable/action_icon_scan"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/apiUrlLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="10"
android:text="API url" />
<EditText
android:id="@+id/apiUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="10"
android:ems="10"
android:inputType="textUri"
android:text="" />
<TextView
android:id="@+id/apiKeyLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="10"
android:text="API key" />
<EditText
android:id="@+id/apiKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="20"
android:autofillHints="password"
android:ems="10"
android:inputType="text"
android:text="" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -12,4 +12,9 @@
android:orderInCategory="200" android:orderInCategory="200"
android:title="@string/action_reset_log" android:title="@string/action_reset_log"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/action_settings"
android:orderInCategory="300"
android:title="@string/settings"
app:showAsAction="never" />
</menu> </menu>

View File

@@ -7,5 +7,11 @@
<string name="second_fragment_label">Second Fragment</string> <string name="second_fragment_label">Second Fragment</string>
<string name="next">Next</string> <string name="next">Next</string>
<string name="previous">Previous</string> <string name="previous">Previous</string>
<string name="settings">Settings</string>
<string name="api_url">API url</string>
<string name="api_key">API key</string>
<string name="save">Save</string>
<string name="cancel">Cancel</string>
<string name="start_scan">Start Scan</string>
</resources> </resources>