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
|
||||
|
||||
import android.app.Application
|
||||
import farm.hobbs.clippyauto.config.AppConfig
|
||||
import farm.hobbs.clippyauto.matrix.MatrixRestClient
|
||||
|
||||
class ClippyAutoApp : Application() {
|
||||
|
||||
lateinit var matrixClient: farm.hobbs.clippyauto.matrix.MatrixRestClient
|
||||
lateinit var matrixClient: MatrixRestClient
|
||||
private set
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
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.LifecycleOwner
|
||||
import farm.hobbs.clippyauto.VoiceLoop
|
||||
import farm.hobbs.clippyauto.config.AppConfig
|
||||
import farm.hobbs.clippyauto.matrix.MatrixRestClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -23,16 +24,6 @@ class ClippySession : Session(), DefaultLifecycleObserver {
|
||||
private var screen: ClippyScreen? = null
|
||||
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 {
|
||||
lifecycle.addObserver(this)
|
||||
val clippyScreen = ClippyScreen(carContext)
|
||||
@@ -41,21 +32,33 @@ class ClippySession : Session(), DefaultLifecycleObserver {
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
val matrixClient = MatrixRestClient(HOMESERVER)
|
||||
val matrixClient = MatrixRestClient(AppConfig.homeserverUrl)
|
||||
|
||||
voiceLoop = VoiceLoop(
|
||||
context = carContext,
|
||||
matrixClient = matrixClient,
|
||||
roomId = ROOM_ID,
|
||||
clippyUserId = CLIPPY_USER,
|
||||
roomId = AppConfig.roomId,
|
||||
clippyUserId = AppConfig.clippyUserId,
|
||||
onStateChange = { _, status ->
|
||||
screen?.updateStatus(status)
|
||||
}
|
||||
)
|
||||
|
||||
// Login then initialize voice loop
|
||||
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) {
|
||||
if (loggedIn) {
|
||||
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.content.ContextCompat
|
||||
import farm.hobbs.clippyauto.VoiceLoop
|
||||
import farm.hobbs.clippyauto.config.AppConfig
|
||||
import farm.hobbs.clippyauto.matrix.MatrixRestClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -26,11 +27,6 @@ import kotlinx.coroutines.withContext
|
||||
class SetupActivity : AppCompatActivity() {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -102,15 +98,15 @@ class SetupActivity : AppCompatActivity() {
|
||||
|
||||
private fun connectToMatrix() {
|
||||
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(
|
||||
context = this,
|
||||
matrixClient = matrixClient,
|
||||
roomId = ROOM_ID,
|
||||
clippyUserId = CLIPPY_USER,
|
||||
roomId = AppConfig.roomId,
|
||||
clippyUserId = AppConfig.clippyUserId,
|
||||
onStateChange = { state, status ->
|
||||
runOnUiThread {
|
||||
statusText.text = status
|
||||
@@ -120,11 +116,24 @@ class SetupActivity : AppCompatActivity() {
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
val loggedIn = withContext(Dispatchers.IO) {
|
||||
matrixClient.login(MATRIX_USER, MATRIX_PASS)
|
||||
// Try stored token first, fall back to login
|
||||
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) {
|
||||
log("✅ Logged in as @clippyauto:hobbs.farm")
|
||||
log("✅ Logged in as @${AppConfig.matrixUser}:hobbs.farm")
|
||||
voiceLoop?.initialize()
|
||||
voiceLoop?.startSession()
|
||||
talkButton.isEnabled = true
|
||||
|
||||
Reference in New Issue
Block a user