From 83b120b1ce879ed004c783a59dfba9e9fe5a1a59 Mon Sep 17 00:00:00 2001 From: Stefan Zollinger Date: Tue, 16 Apr 2024 18:22:05 +0200 Subject: [PATCH] feat: add api configuration settings --- app/build.gradle | 1 + .../vc/kirbytest/ConnectionManager.kt | 8 +- .../com/logitech/vc/kirbytest/KirbyDevice.kt | 10 +- .../com/logitech/vc/kirbytest/MainActivity.kt | 48 ++++--- .../com/logitech/vc/kirbytest/Settings.kt | 117 ++++++++++++++++++ app/src/main/res/layout/activity_main.xml | 2 +- app/src/main/res/layout/settings_dialog.xml | 56 +++++++++ app/src/main/res/menu/menu_main.xml | 5 + app/src/main/res/values/strings.xml | 6 + 9 files changed, 231 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/logitech/vc/kirbytest/Settings.kt create mode 100644 app/src/main/res/layout/settings_dialog.xml diff --git a/app/build.gradle b/app/build.gradle index 130538d..bc2e674 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,4 +64,5 @@ dependencies { implementation group: 'commons-codec', name: 'commons-codec', version: '1.16.0' implementation "com.android.volley:volley:1.2.1" + implementation "androidx.datastore:datastore-preferences:1.0.0" } \ No newline at end of file diff --git a/app/src/main/java/com/logitech/vc/kirbytest/ConnectionManager.kt b/app/src/main/java/com/logitech/vc/kirbytest/ConnectionManager.kt index 442d255..74ff75c 100644 --- a/app/src/main/java/com/logitech/vc/kirbytest/ConnectionManager.kt +++ b/app/src/main/java/com/logitech/vc/kirbytest/ConnectionManager.kt @@ -212,8 +212,6 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) { .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() - private val scanFilters = listOf( ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(BuildConfig.SERVICE_UUID)).build()) - private val bleScanner by lazy { bleAdapter.bluetoothLeScanner } @@ -353,6 +351,9 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) { } fun discoverServices(device: BluetoothDevice) { + if(operationQueue.any { it.device.address === device.address && it is DiscoverServicesRequest }) { + return + } enqueueOperation(DiscoverServicesRequest(device)) } @@ -377,6 +378,9 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) { } fun readRemoteRssi(device: BluetoothDevice) { + if(operationQueue.any { it.device.address === device.address && it is ReadRemoteRssi }) { + return + } enqueueOperation(ReadRemoteRssi(device)) } diff --git a/app/src/main/java/com/logitech/vc/kirbytest/KirbyDevice.kt b/app/src/main/java/com/logitech/vc/kirbytest/KirbyDevice.kt index 8fc7469..4ae4a4e 100644 --- a/app/src/main/java/com/logitech/vc/kirbytest/KirbyDevice.kt +++ b/app/src/main/java/com/logitech/vc/kirbytest/KirbyDevice.kt @@ -1,5 +1,6 @@ package com.logitech.vc.kirbytest +import SettingsRepository import android.annotation.SuppressLint import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice.BOND_BONDED @@ -38,9 +39,11 @@ class KirbyDevice( private val connectionManager: ConnectionManager, private val bleDevice: BluetoothDevice, private val loggerDb: LoggerContract.LoggerDb, + private val settingsRepository: SettingsRepository, private val onStateChange: (device: KirbyDevice) -> Unit, + ) : BleListener(bleDevice.address), DeviceListEntry { private val queue: RequestQueue = Volley.newRequestQueue(context) @@ -164,10 +167,11 @@ class KirbyDevice( } private fun publishMeasurement(payload: String) { - val accessKey = BuildConfig.API_KEY - val url = BuildConfig.API_BASE_URL + val settings = settingsRepository.readSettings() + val accessKey = settings.apiKey + val url = settings.apiUrl - if(url.isEmpty()) { + if(url.isEmpty() || accessKey.isEmpty()) { return } diff --git a/app/src/main/java/com/logitech/vc/kirbytest/MainActivity.kt b/app/src/main/java/com/logitech/vc/kirbytest/MainActivity.kt index 8ed716f..b719f01 100644 --- a/app/src/main/java/com/logitech/vc/kirbytest/MainActivity.kt +++ b/app/src/main/java/com/logitech/vc/kirbytest/MainActivity.kt @@ -1,5 +1,6 @@ package com.logitech.vc.kirbytest +import SettingsRepository import android.Manifest import android.annotation.SuppressLint import android.app.Activity @@ -15,7 +16,6 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Handler import android.util.Log import android.view.Menu import android.view.MenuItem @@ -23,7 +23,6 @@ import android.view.View.INVISIBLE import android.view.View.VISIBLE import android.widget.Toast import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity @@ -38,6 +37,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import settingsDialog import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.Timer @@ -69,6 +69,7 @@ class MainActivity : AppCompatActivity() { private lateinit var loggerDb: LoggerContract.LoggerDb private lateinit var createFileLauncher: ActivityResultLauncher private val bondedDevices = HashSet() + private lateinit var settings: SettingsRepository private val bluetoothAdapter: BluetoothAdapter by lazy { val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager @@ -93,7 +94,7 @@ class MainActivity : AppCompatActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) super.onCreate(savedInstanceState) - + settings = SettingsRepository(applicationContext) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) @@ -136,23 +137,30 @@ class MainActivity : AppCompatActivity() { } } - if(hasRequiredRuntimePermissions()) { - connectionManager.startScan() + if (hasRequiredRuntimePermissions()) { + connectionManager.startScan() - lifecycleScope.launch { + lifecycleScope.launch { - withContext(Dispatchers.Main) { - delay(5000L) - connectionManager.stopScan() - } + withContext(Dispatchers.Main) { + delay(5000L) + connectionManager.stopScan() } } + } Timer().schedule(object : TimerTask() { override fun run() { loggerDb.exportToMultipleCSV() } }, 1000, 10000) + + + + + lifecycleScope.launch { + settings.loadSettings() + } } private fun toggleScanning(): Unit { @@ -239,7 +247,8 @@ class MainActivity : AppCompatActivity() { Log.i("KirbyDevice", "Connecting to " + result.device.address) connectionManager.connect(result.device) - connectionManager.readRemoteRssi(result.device) + + //connectionManager.readRemoteRssi(result.device) connectionManager.discoverServices(result.device) } @@ -253,12 +262,13 @@ class MainActivity : AppCompatActivity() { bleDevice: BluetoothDevice ): KirbyDevice { - val device = KirbyDevice(this.applicationContext, connectionManager, bleDevice, loggerDb) { - val i = kirbyDevices.indexOfFirst { d -> d === it } - runOnUiThread { - deviceListAdapter.notifyItemChanged(i) + val device = + KirbyDevice(this.applicationContext, connectionManager, bleDevice, loggerDb, settings) { + val i = kirbyDevices.indexOfFirst { d -> d === it } + runOnUiThread { + deviceListAdapter.notifyItemChanged(i) + } } - } Log.i("MainActivity", bleDevice.address) connectionManager.register(device) @@ -303,6 +313,12 @@ class MainActivity : AppCompatActivity() { return true } + R.id.action_settings -> { + val settingsDialog = settingsDialog(this, settings) + settingsDialog.show() + return true + } + else -> super.onOptionsItemSelected(item) } } diff --git a/app/src/main/java/com/logitech/vc/kirbytest/Settings.kt b/app/src/main/java/com/logitech/vc/kirbytest/Settings.kt new file mode 100644 index 0000000..049acd6 --- /dev/null +++ b/app/src/main/java/com/logitech/vc/kirbytest/Settings.kt @@ -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 = 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(R.id.apiUrl) + val keyField = root.findViewById(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 +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c125121..0bce1b8 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -44,7 +44,7 @@ android:id="@+id/fab" android:layout_width="150dp" android:layout_height="match_parent" - android:text='Start Scan' + android:text='@string/start_scan' app:icon="@drawable/action_icon_scan" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/settings_dialog.xml b/app/src/main/res/layout/settings_dialog.xml new file mode 100644 index 0000000..e068cb8 --- /dev/null +++ b/app/src/main/res/layout/settings_dialog.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index 8d99837..6e9a50e 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -12,4 +12,9 @@ android:orderInCategory="200" android:title="@string/action_reset_log" app:showAsAction="never" /> + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b65df0..1688352 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,5 +7,11 @@ Second Fragment Next Previous + Settings + API url + API key + Save + Cancel + Start Scan \ No newline at end of file