Centralize config + EncryptedSharedPreferences for credentials
ClippyAuto CI / build (push) Has been cancelled
ClippyAuto CI / build (push) Has been cancelled
- New AppConfig singleton backed by EncryptedSharedPreferences - Credentials stored in Android Keystore (AES256-GCM) - Removed hardcoded creds from ClippySession and SetupActivity - Access token caching to avoid re-login on each session - Defaults seeded on first run for backward compatibility Co-authored-by: Spirit <spirit@hobbs.farm>
This commit is contained in:
@@ -1,17 +1,23 @@
|
|||||||
package farm.hobbs.clippyauto
|
package farm.hobbs.clippyauto
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import farm.hobbs.clippyauto.config.AppConfig
|
||||||
|
import farm.hobbs.clippyauto.matrix.MatrixRestClient
|
||||||
|
|
||||||
class ClippyAutoApp : Application() {
|
class ClippyAutoApp : Application() {
|
||||||
|
|
||||||
lateinit var matrixClient: farm.hobbs.clippyauto.matrix.MatrixRestClient
|
lateinit var matrixClient: MatrixRestClient
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
instance = this
|
instance = this
|
||||||
matrixClient = farm.hobbs.clippyauto.matrix.MatrixRestClient(
|
|
||||||
homeserverUrl = "https://mess.hobbs.farm"
|
// Initialize encrypted config before anything else
|
||||||
|
AppConfig.init(this)
|
||||||
|
|
||||||
|
matrixClient = MatrixRestClient(
|
||||||
|
homeserverUrl = AppConfig.homeserverUrl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.car.app.Session
|
|||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import farm.hobbs.clippyauto.VoiceLoop
|
import farm.hobbs.clippyauto.VoiceLoop
|
||||||
|
import farm.hobbs.clippyauto.config.AppConfig
|
||||||
import farm.hobbs.clippyauto.matrix.MatrixRestClient
|
import farm.hobbs.clippyauto.matrix.MatrixRestClient
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -23,16 +24,6 @@ class ClippySession : Session(), DefaultLifecycleObserver {
|
|||||||
private var screen: ClippyScreen? = null
|
private var screen: ClippyScreen? = null
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
// TODO: Move to SharedPreferences via SetupActivity
|
|
||||||
companion object {
|
|
||||||
const val HOMESERVER = "https://mess.hobbs.farm"
|
|
||||||
const val ROOM_ID = "!KSovCtghshGUfVDbQg:hobbs.farm"
|
|
||||||
const val CLIPPY_USER = "@clippy:hobbs.farm"
|
|
||||||
// Hardcoded for v1 — move to secure storage (EncryptedSharedPreferences)
|
|
||||||
const val MATRIX_USER = "clippyauto"
|
|
||||||
const val MATRIX_PASS = "ClippyAuto2026!"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateScreen(intent: Intent): Screen {
|
override fun onCreateScreen(intent: Intent): Screen {
|
||||||
lifecycle.addObserver(this)
|
lifecycle.addObserver(this)
|
||||||
val clippyScreen = ClippyScreen(carContext)
|
val clippyScreen = ClippyScreen(carContext)
|
||||||
@@ -41,21 +32,33 @@ class ClippySession : Session(), DefaultLifecycleObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
val matrixClient = MatrixRestClient(HOMESERVER)
|
val matrixClient = MatrixRestClient(AppConfig.homeserverUrl)
|
||||||
|
|
||||||
voiceLoop = VoiceLoop(
|
voiceLoop = VoiceLoop(
|
||||||
context = carContext,
|
context = carContext,
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
roomId = ROOM_ID,
|
roomId = AppConfig.roomId,
|
||||||
clippyUserId = CLIPPY_USER,
|
clippyUserId = AppConfig.clippyUserId,
|
||||||
onStateChange = { _, status ->
|
onStateChange = { _, status ->
|
||||||
screen?.updateStatus(status)
|
screen?.updateStatus(status)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Login then initialize voice loop
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val loggedIn = matrixClient.login(MATRIX_USER, MATRIX_PASS)
|
// Try stored token first, fall back to login
|
||||||
|
val storedToken = AppConfig.matrixAccessToken
|
||||||
|
val loggedIn = if (storedToken != null) {
|
||||||
|
matrixClient.loginWithToken(storedToken)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
val success = matrixClient.login(AppConfig.matrixUser, AppConfig.matrixPass)
|
||||||
|
if (success) {
|
||||||
|
// Cache the token for next session
|
||||||
|
AppConfig.matrixAccessToken = matrixClient.accessToken
|
||||||
|
}
|
||||||
|
success
|
||||||
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
voiceLoop?.initialize()
|
voiceLoop?.initialize()
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package farm.hobbs.clippyauto.config
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
|
import androidx.security.crypto.MasterKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized configuration for ClippyAuto.
|
||||||
|
*
|
||||||
|
* Credentials are stored in EncryptedSharedPreferences (Android Keystore-backed).
|
||||||
|
* On first run, defaults are seeded so existing behavior is preserved.
|
||||||
|
* SetupActivity can update these values in the future.
|
||||||
|
*/
|
||||||
|
object AppConfig {
|
||||||
|
|
||||||
|
private const val PREFS_NAME = "clippy_secure_prefs"
|
||||||
|
|
||||||
|
// Keys
|
||||||
|
private const val KEY_HOMESERVER = "homeserver_url"
|
||||||
|
private const val KEY_ROOM_ID = "room_id"
|
||||||
|
private const val KEY_CLIPPY_USER = "clippy_user_id"
|
||||||
|
private const val KEY_MATRIX_USER = "matrix_username"
|
||||||
|
private const val KEY_MATRIX_PASS = "matrix_password"
|
||||||
|
private const val KEY_MATRIX_TOKEN = "matrix_access_token"
|
||||||
|
private const val KEY_INITIALIZED = "config_initialized"
|
||||||
|
|
||||||
|
// Defaults (v1 — will be replaced by SetupActivity input)
|
||||||
|
private const val DEFAULT_HOMESERVER = "https://mess.hobbs.farm"
|
||||||
|
private const val DEFAULT_ROOM_ID = "!KSovCtghshGUfVDbQg:hobbs.farm"
|
||||||
|
private const val DEFAULT_CLIPPY_USER = "@clippy:hobbs.farm"
|
||||||
|
private const val DEFAULT_MATRIX_USER = "clippyauto"
|
||||||
|
private const val DEFAULT_MATRIX_PASS = "ClippyAuto2026!"
|
||||||
|
|
||||||
|
private var prefs: SharedPreferences? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the config. Must be called from Application.onCreate() or before first use.
|
||||||
|
*/
|
||||||
|
fun init(context: Context) {
|
||||||
|
if (prefs != null) return
|
||||||
|
|
||||||
|
val masterKey = MasterKey.Builder(context)
|
||||||
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
prefs = EncryptedSharedPreferences.create(
|
||||||
|
context,
|
||||||
|
PREFS_NAME,
|
||||||
|
masterKey,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
|
)
|
||||||
|
|
||||||
|
// Seed defaults on first run
|
||||||
|
if (!prefs!!.getBoolean(KEY_INITIALIZED, false)) {
|
||||||
|
prefs!!.edit()
|
||||||
|
.putString(KEY_HOMESERVER, DEFAULT_HOMESERVER)
|
||||||
|
.putString(KEY_ROOM_ID, DEFAULT_ROOM_ID)
|
||||||
|
.putString(KEY_CLIPPY_USER, DEFAULT_CLIPPY_USER)
|
||||||
|
.putString(KEY_MATRIX_USER, DEFAULT_MATRIX_USER)
|
||||||
|
.putString(KEY_MATRIX_PASS, DEFAULT_MATRIX_PASS)
|
||||||
|
.putBoolean(KEY_INITIALIZED, true)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val homeserverUrl: String
|
||||||
|
get() = prefs?.getString(KEY_HOMESERVER, DEFAULT_HOMESERVER) ?: DEFAULT_HOMESERVER
|
||||||
|
|
||||||
|
val roomId: String
|
||||||
|
get() = prefs?.getString(KEY_ROOM_ID, DEFAULT_ROOM_ID) ?: DEFAULT_ROOM_ID
|
||||||
|
|
||||||
|
val clippyUserId: String
|
||||||
|
get() = prefs?.getString(KEY_CLIPPY_USER, DEFAULT_CLIPPY_USER) ?: DEFAULT_CLIPPY_USER
|
||||||
|
|
||||||
|
val matrixUser: String
|
||||||
|
get() = prefs?.getString(KEY_MATRIX_USER, DEFAULT_MATRIX_USER) ?: DEFAULT_MATRIX_USER
|
||||||
|
|
||||||
|
val matrixPass: String
|
||||||
|
get() = prefs?.getString(KEY_MATRIX_PASS, DEFAULT_MATRIX_PASS) ?: DEFAULT_MATRIX_PASS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a Matrix access token for reuse (avoids re-login).
|
||||||
|
*/
|
||||||
|
var matrixAccessToken: String?
|
||||||
|
get() = prefs?.getString(KEY_MATRIX_TOKEN, null)
|
||||||
|
set(value) {
|
||||||
|
prefs?.edit()?.putString(KEY_MATRIX_TOKEN, value)?.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update credentials (for future SetupActivity use).
|
||||||
|
*/
|
||||||
|
fun updateCredentials(
|
||||||
|
homeserver: String? = null,
|
||||||
|
roomId: String? = null,
|
||||||
|
username: String? = null,
|
||||||
|
password: String? = null
|
||||||
|
) {
|
||||||
|
val editor = prefs?.edit() ?: return
|
||||||
|
homeserver?.let { editor.putString(KEY_HOMESERVER, it) }
|
||||||
|
roomId?.let { editor.putString(KEY_ROOM_ID, it) }
|
||||||
|
username?.let { editor.putString(KEY_MATRIX_USER, it) }
|
||||||
|
password?.let { editor.putString(KEY_MATRIX_PASS, it) }
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import farm.hobbs.clippyauto.VoiceLoop
|
import farm.hobbs.clippyauto.VoiceLoop
|
||||||
|
import farm.hobbs.clippyauto.config.AppConfig
|
||||||
import farm.hobbs.clippyauto.matrix.MatrixRestClient
|
import farm.hobbs.clippyauto.matrix.MatrixRestClient
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -26,11 +27,6 @@ import kotlinx.coroutines.withContext
|
|||||||
class SetupActivity : AppCompatActivity() {
|
class SetupActivity : AppCompatActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val HOMESERVER = "https://mess.hobbs.farm"
|
|
||||||
private const val ROOM_ID = "!KSovCtghshGUfVDbQg:hobbs.farm"
|
|
||||||
private const val CLIPPY_USER = "@clippy:hobbs.farm"
|
|
||||||
private const val MATRIX_USER = "clippyauto"
|
|
||||||
private const val MATRIX_PASS = "ClippyAuto2026!"
|
|
||||||
private const val MIC_PERMISSION_CODE = 100
|
private const val MIC_PERMISSION_CODE = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,15 +98,15 @@ class SetupActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun connectToMatrix() {
|
private fun connectToMatrix() {
|
||||||
statusText.text = "Connecting to Matrix..."
|
statusText.text = "Connecting to Matrix..."
|
||||||
log("Connecting to $HOMESERVER...")
|
log("Connecting to ${AppConfig.homeserverUrl}...")
|
||||||
|
|
||||||
val matrixClient = MatrixRestClient(HOMESERVER)
|
val matrixClient = MatrixRestClient(AppConfig.homeserverUrl)
|
||||||
|
|
||||||
voiceLoop = VoiceLoop(
|
voiceLoop = VoiceLoop(
|
||||||
context = this,
|
context = this,
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
roomId = ROOM_ID,
|
roomId = AppConfig.roomId,
|
||||||
clippyUserId = CLIPPY_USER,
|
clippyUserId = AppConfig.clippyUserId,
|
||||||
onStateChange = { state, status ->
|
onStateChange = { state, status ->
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
statusText.text = status
|
statusText.text = status
|
||||||
@@ -120,11 +116,24 @@ class SetupActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val loggedIn = withContext(Dispatchers.IO) {
|
// Try stored token first, fall back to login
|
||||||
matrixClient.login(MATRIX_USER, MATRIX_PASS)
|
val storedToken = AppConfig.matrixAccessToken
|
||||||
|
val loggedIn = if (storedToken != null) {
|
||||||
|
log("Using cached access token")
|
||||||
|
matrixClient.loginWithToken(storedToken)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
val success = withContext(Dispatchers.IO) {
|
||||||
|
matrixClient.login(AppConfig.matrixUser, AppConfig.matrixPass)
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
AppConfig.matrixAccessToken = matrixClient.accessToken
|
||||||
|
}
|
||||||
|
success
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
log("✅ Logged in as @clippyauto:hobbs.farm")
|
log("✅ Logged in as @${AppConfig.matrixUser}:hobbs.farm")
|
||||||
voiceLoop?.initialize()
|
voiceLoop?.initialize()
|
||||||
voiceLoop?.startSession()
|
voiceLoop?.startSession()
|
||||||
talkButton.isEnabled = true
|
talkButton.isEnabled = true
|
||||||
|
|||||||
Reference in New Issue
Block a user