Add privacy mode setting to disable logs and activity stats

Adds a privacy mode toggle in the Security screen that disables logging
and activity statistics. When enabled, existing logs and activity history
are wiped immediately and future writes are blocked at the DAO layer.
Translated into all 13 supported locales.
This commit is contained in:
greenart7c3
2026-07-06 17:34:48 -03:00
parent 1824bb67a8
commit b3d420819e
26 changed files with 126 additions and 15 deletions
@@ -67,6 +67,7 @@ private enum class SettingsKeys(val key: String) {
UPDATE_CHANNEL("update_channel"),
LAST_UPDATE_CHECK_TIME("last_update_check_time"),
START_SERVICE_ON_BOOT("start_service_on_boot"),
PRIVACY_MODE("privacy_mode"),
WEBDAV_URL("webdav_url"),
WEBDAV_USERNAME("webdav_username"),
WEBDAV_PASSWORD_ENCRYPTED("webdav_password_encrypted"),
@@ -151,6 +152,7 @@ object LocalPreferences {
putString(SettingsKeys.LANGUAGE_PREFS.key, settings.language)
putStringSet(SettingsKeys.AUTH_WHITELIST.key, settings.authWhitelist.toSet())
putBoolean(SettingsKeys.START_SERVICE_ON_BOOT.key, settings.startServiceOnBoot)
putBoolean(SettingsKeys.PRIVACY_MODE.key, settings.privacyMode)
putBoolean(SettingsKeys.RATE_LIMIT_ENABLED.key, settings.rateLimitEnabled)
putInt(SettingsKeys.RATE_LIMIT_MAX_PER_WINDOW.key, settings.rateLimitMaxPerWindow)
putInt(SettingsKeys.RATE_LIMIT_WINDOW_SECONDS.key, settings.rateLimitWindowSeconds)
@@ -306,6 +308,7 @@ object LocalPreferences {
UpdateChannel.STABLE
},
startServiceOnBoot = getBoolean(SettingsKeys.START_SERVICE_ON_BOOT.key, true),
privacyMode = getBoolean(SettingsKeys.PRIVACY_MODE.key, false),
rateLimitEnabled = getBoolean(SettingsKeys.RATE_LIMIT_ENABLED.key, true),
rateLimitMaxPerWindow = getInt(SettingsKeys.RATE_LIMIT_MAX_PER_WINDOW.key, 5),
rateLimitWindowSeconds = getInt(SettingsKeys.RATE_LIMIT_WINDOW_SECONDS.key, 30),
@@ -584,6 +587,30 @@ object LocalPreferences {
Amber.instance.settings = loadSettingsFromEncryptedStorage()
}
/**
* Toggles privacy mode. When enabled, immediately wipes all existing logs and
* activity history for every account so no previously collected data survives.
* Future writes are blocked at the DAO layer while this setting stays on.
*/
suspend fun updatePrivacyMode(context: Context, enabled: Boolean) {
sharedPrefs(context).edit {
apply {
putBoolean(SettingsKeys.PRIVACY_MODE.key, enabled)
}
}
Amber.instance.settings = loadSettingsFromEncryptedStorage()
if (enabled) {
allSavedAccounts(context).forEach {
try {
Amber.instance.getLogDatabase(it.npub).dao().clearLogs()
Amber.instance.getHistoryDatabase(it.npub).dao().clearHistory()
} catch (e: Exception) {
AmberLog.e(Amber.TAG, "Error wiping data for privacy mode", e)
}
}
}
}
fun updateUpdateCheckFrequency(context: Context, frequency: UpdateCheckFrequency) {
sharedPrefs(context).edit {
apply {
@@ -303,7 +303,7 @@ object SignerProviderQuery {
}
} catch (e: Exception) {
scope.launch {
logDatabase.dao().insertLog(
logDatabase.dao().insertLogIfEnabled(
LogEntity(
0,
requesterId,
@@ -407,7 +407,7 @@ object SignerProviderQuery {
}
} catch (e: Exception) {
scope.launch {
logDatabase.dao().insertLog(
logDatabase.dao().insertLogIfEnabled(
LogEntity(
0,
requesterId,
@@ -507,7 +507,7 @@ object SignerProviderQuery {
AmberLog.d(Amber.TAG, "Failed to sign psbt", e)
scope.launch {
val logDb = Amber.instance.getLogDatabase(account.npub)
logDb.dao().insertLog(
logDb.dao().insertLogIfEnabled(
LogEntity(
0,
requesterId,
@@ -624,7 +624,7 @@ object SignerProviderQuery {
scope.launch {
LocalPreferences.allSavedAccounts(context).forEach { accInfo ->
val database = Amber.instance.getLogDatabase(accInfo.npub)
database.dao().insertLog(
database.dao().insertLogIfEnabled(
LogEntity(
0,
requesterId,
@@ -97,6 +97,10 @@ interface HistoryDao {
@Transaction
suspend fun deleteOldHistory(time: Long): Int
@Query("DELETE FROM history")
@Transaction
suspend fun clearHistory()
@Delete
@Transaction
suspend fun deleteHistory(historyEntity: HistoryEntity)
@@ -108,6 +112,7 @@ interface HistoryDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
@Transaction
suspend fun addHistory(entities: List<HistoryEntity>, npub: String?) {
if (Amber.instance.settings.privacyMode) return
try {
val localEntities = entities.map { entity ->
val permission = Permission(entity.type.toLowerCase(Locale.current), entity.kind)
@@ -6,6 +6,7 @@ import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import com.greenart7c3.nostrsigner.Amber
import kotlinx.coroutines.flow.Flow
@Dao
@@ -17,6 +18,17 @@ interface LogDao {
@Transaction
suspend fun insertLog(logEntity: LogEntity)
/**
* Inserts a log entry unless privacy mode is on, in which case the write is
* dropped. All call sites that record non-essential relay/bunker/request
* logs should use this instead of [insertLog].
*/
@Transaction
suspend fun insertLogIfEnabled(logEntity: LogEntity) {
if (Amber.instance.settings.privacyMode) return
insertLog(logEntity)
}
@Query("SELECT * FROM amber_log ORDER BY time DESC")
fun getLogs(): Flow<List<LogEntity>>
@@ -34,6 +34,7 @@ data class AmberSettings(
val updateCheckFrequency: UpdateCheckFrequency = UpdateCheckFrequency.DAILY,
val updateChannel: UpdateChannel = UpdateChannel.STABLE,
val startServiceOnBoot: Boolean = true,
val privacyMode: Boolean = false,
val rateLimitEnabled: Boolean = true,
val rateLimitMaxPerWindow: Int = 5,
val rateLimitWindowSeconds: Int = 30,
@@ -94,7 +94,7 @@ class NostrClientLoggerListener(
scope.launch {
val account = LocalPreferences.currentAccount(context)
if (account != null) {
Amber.instance.getLogDatabase(account).dao().insertLog(
Amber.instance.getLogDatabase(account).dao().insertLogIfEnabled(
LogEntity(
id = 0,
url = url,
@@ -105,7 +105,7 @@ class NostrClientLoggerListener(
)
} else {
LocalPreferences.allSavedAccounts(context).forEach {
Amber.instance.getLogDatabase(it.npub).dao().insertLog(
Amber.instance.getLogDatabase(it.npub).dao().insertLogIfEnabled(
LogEntity(
id = 0,
url = url,
@@ -82,7 +82,7 @@ object BunkerRequestUtils {
if (relays.isEmpty()) {
onDone(true)
onLoading(false)
Amber.instance.getLogDatabase(account.npub).dao().insertLog(
Amber.instance.getLogDatabase(account.npub).dao().insertLogIfEnabled(
LogEntity(
id = 0,
url = bunkerRequest.localKey,
@@ -109,7 +109,7 @@ object BunkerRequestUtils {
// plaintext. Record only non-sensitive metadata (id + error status).
val sanitizedResponse = "id=${bunkerResponse.id} ${bunkerResponse.error?.let { "error=$it" } ?: "ok"}"
relays.forEach { relay ->
Amber.instance.getLogDatabase(account.npub).dao().insertLog(
Amber.instance.getLogDatabase(account.npub).dao().insertLogIfEnabled(
LogEntity(
id = 0,
url = relay.url,
@@ -51,7 +51,7 @@ class ClearLogsWorker(appContext: Context, workerParams: WorkerParameters) : Cor
dao.updateExpiredPermissions(TimeUtils.now())
val deleted = dao.deleteOldApplications(now / 1000)
if (deleted > 0) {
logDao.insertLog(
logDao.insertLogIfEnabled(
LogEntity(
id = 0,
url = "",
@@ -80,7 +80,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
Amber.instance.applicationIOScope.launch {
if (npub != null) {
val dao = Amber.instance.getLogDatabase(npub).dao()
dao.insertLog(
dao.insertLogIfEnabled(
LogEntity(
0,
url,
@@ -94,7 +94,7 @@ class EventNotificationConsumer(private val applicationContext: Context) {
accounts.forEach {
LocalPreferences.loadFromEncryptedStorage(applicationContext, it.npub)?.let { acc ->
val dao = Amber.instance.getLogDatabase(acc.npub).dao()
dao.insertLog(
dao.insertLogIfEnabled(
LogEntity(
0,
url,
@@ -313,7 +313,7 @@ object IntentUtils {
}
Amber.instance.applicationIOScope.launch {
val database = Amber.instance.getLogDatabase(account.npub)
database.dao().insertLog(
database.dao().insertLogIfEnabled(
LogEntity(
0,
packageName ?: "",
@@ -498,7 +498,7 @@ object IntentUtils {
}
Amber.instance.applicationIOScope.launch {
val database = Amber.instance.getLogDatabase(account.npub)
database.dao().insertLog(
database.dao().insertLogIfEnabled(
LogEntity(
0,
packageName ?: "",
@@ -758,7 +758,7 @@ object IntentUtils {
emitInvalid(intent, packageName, "Error parsing intent: ${e.message}", e)
Amber.instance.applicationIOScope.launch {
LocalPreferences.allSavedAccounts(Amber.instance).forEach {
Amber.instance.getLogDatabase(it.npub).dao().insertLog(
Amber.instance.getLogDatabase(it.npub).dao().insertLogIfEnabled(
LogEntity(
id = 0,
url = "IntentUtils",
@@ -159,7 +159,7 @@ object NostrConnectUtils {
} catch (e: Exception) {
AmberLog.e(Amber.TAG, e.message, e)
Amber.instance.applicationIOScope.launch {
Amber.instance.getLogDatabase(account.npub).dao().insertLog(
Amber.instance.getLogDatabase(account.npub).dao().insertLogIfEnabled(
LogEntity(
0,
"nostrconnect",
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.material3.MaterialTheme
import androidx.compose.material3.Surface
@@ -20,6 +21,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
@@ -47,10 +50,12 @@ fun SecurityScreen(
)
var enableBiometrics by remember { mutableStateOf(Amber.instance.settings.useAuth) }
val setupPin by remember { mutableStateOf(Amber.instance.settings.usePin) }
var privacyMode by remember { mutableStateOf(Amber.instance.settings.privacyMode) }
var biometricsIndex by remember {
mutableIntStateOf(Amber.instance.settings.biometricsTimeType.screenCode)
}
val scope = rememberCoroutineScope()
val context = LocalContext.current
Surface(
modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
@@ -83,6 +88,39 @@ fun SecurityScreen(
)
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
.clickable {
val newValue = !privacyMode
privacyMode = newValue
scope.launch(Dispatchers.IO) {
LocalPreferences.updatePrivacyMode(context, newValue)
}
},
) {
Column(modifier = Modifier.weight(1f)) {
Text(text = stringResource(R.string.privacy_mode))
Text(
text = stringResource(R.string.privacy_mode_description),
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
)
}
Switch(
checked = privacyMode,
onCheckedChange = { enabled ->
privacyMode = enabled
scope.launch(Dispatchers.IO) {
LocalPreferences.updatePrivacyMode(context, enabled)
}
},
)
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
+2
View File
@@ -221,6 +221,8 @@
<string name="authenticate">Authentifizierung benötigt, um die App zu nutzen</string>
<string name="security">Sicherheit</string>
<string name="enable_biometrics">Biometrische Daten aktivieren</string>
<string name="privacy_mode">Datenschutzmodus</string>
<string name="privacy_mode_description">Deaktiviert die Protokollierung und Aktivitätsstatistiken. Beim Einschalten werden vorhandene Protokolle und Aktivitätsverläufe gelöscht.</string>
<string name="when_to_ask">Wer soll gefragt werden</string>
<string name="every_time">Jedes Mal</string>
<string name="one_minute">Eine Minute</string>
+2
View File
@@ -224,6 +224,8 @@
<string name="authenticate">Autenticación necesaria para usar la aplicación</string>
<string name="security">Seguridad</string>
<string name="enable_biometrics">Activar biometría</string>
<string name="privacy_mode">Modo de privacidad</string>
<string name="privacy_mode_description">Desactiva el registro de logs y las estadísticas de actividad. Los logs y el historial de actividad existentes se eliminan al activarse.</string>
<string name="when_to_ask">Cuándo pedir</string>
<string name="every_time">Siempre</string>
<string name="one_minute">Un minuto</string>
+2
View File
@@ -221,6 +221,8 @@
<string name="authenticate">Authentification nécessaire pour utiliser l\'application</string>
<string name="security">Sécurité</string>
<string name="enable_biometrics">Activer la biométrie</string>
<string name="privacy_mode">Mode de confidentialité</string>
<string name="privacy_mode_description">Désactive la journalisation et les statistiques d\'activité. Les journaux et l\'historique d\'activité existants sont supprimés lors de l\'activation.</string>
<string name="when_to_ask">Quand demander</string>
<string name="every_time">À chaque fois</string>
<string name="one_minute">Une minute</string>
+2
View File
@@ -224,6 +224,8 @@
<string name="authenticate">Autentikasi diperlukan untuk menggunakan aplikasi</string>
<string name="security">Keamanan</string>
<string name="enable_biometrics">Aktifkan biometrik</string>
<string name="privacy_mode">Mode privasi</string>
<string name="privacy_mode_description">Menonaktifkan pencatatan log dan statistik aktivitas. Log dan riwayat aktivitas yang ada dihapus saat diaktifkan.</string>
<string name="when_to_ask">Kapan bertanya</string>
<string name="every_time">Setiap kali</string>
<string name="one_minute">Satu menit</string>
@@ -224,6 +224,8 @@
<string name="authenticate">Autenticazione necessaria per usare l\'app</string>
<string name="security">Sicurezza</string>
<string name="enable_biometrics">Abilita biometria</string>
<string name="privacy_mode">Modalità privacy</string>
<string name="privacy_mode_description">Disattiva la registrazione dei log e le statistiche di attività. I log e la cronologia delle attività esistenti vengono eliminati quando viene attivata.</string>
<string name="when_to_ask">Quando chiedere</string>
<string name="every_time">Ogni volta</string>
<string name="one_minute">Un minuto</string>
+2
View File
@@ -200,6 +200,8 @@
<string name="authenticate">アプリを使用するには認証が必要です</string>
<string name="security">セキュリティ</string>
<string name="enable_biometrics">生体認証を有効にする</string>
<string name="privacy_mode">プライバシーモード</string>
<string name="privacy_mode_description">ログ記録とアクティビティ統計を無効にします。オンにすると既存のログとアクティビティ履歴が削除されます。</string>
<string name="when_to_ask">確認のタイミング</string>
<string name="every_time">毎回</string>
<string name="one_minute">1 分</string>
@@ -224,6 +224,8 @@
<string name="authenticate">앱을 사용하려면 인증이 필요합니다</string>
<string name="security">보안</string>
<string name="enable_biometrics">생체 인식 활성화</string>
<string name="privacy_mode">개인정보 보호 모드</string>
<string name="privacy_mode_description">로깅 및 활동 통계를 비활성화합니다. 켜면 기존 로그 및 활동 기록이 삭제됩니다.</string>
<string name="when_to_ask">인증 요청 시점</string>
<string name="every_time">매번</string>
<string name="one_minute">1분</string>
@@ -219,6 +219,8 @@
<string name="authenticate">Autenticação necessária para usar o aplicativo</string>
<string name="security">Segurança</string>
<string name="enable_biometrics">Habilitar biometria</string>
<string name="privacy_mode">Modo de privacidade</string>
<string name="privacy_mode_description">Desativa o registro de logs e estatísticas de atividade. Os logs e o histórico de atividade existentes são excluídos quando ativado.</string>
<string name="when_to_ask">Quando perguntar</string>
<string name="every_time">Sempre</string>
<string name="one_minute">Um minuto</string>
+2
View File
@@ -224,6 +224,8 @@
<string name="authenticate">Требуется аутентификация для использования приложения</string>
<string name="security">Безопасность</string>
<string name="enable_biometrics">Включить биометрию</string>
<string name="privacy_mode">Конфиденциальный режим</string>
<string name="privacy_mode_description">Отключает ведение журнала и статистику активности. При включении существующие журналы и история активности удаляются.</string>
<string name="when_to_ask">Когда спрашивать</string>
<string name="every_time">Каждый раз</string>
<string name="one_minute">Одна минута</string>
+2
View File
@@ -200,6 +200,8 @@
<string name="authenticate">ต้องการการยืนยันตัวตนเพื่อใช้แอป</string>
<string name="security">ความปลอดภัย</string>
<string name="enable_biometrics">เปิดใช้งานไบโอเมตริก</string>
<string name="privacy_mode">โหมดความเป็นส่วนตัว</string>
<string name="privacy_mode_description">ปิดการบันทึกและสถิติการใช้งาน บันทึกและประวัติการใช้งานที่มีอยู่จะถูกลบเมื่อเปิดใช้งาน</string>
<string name="when_to_ask">เมื่อใดที่จะถาม</string>
<string name="every_time">ทุกครั้ง</string>
<string name="one_minute">หนึ่งนาที</string>
+2
View File
@@ -220,6 +220,8 @@
<string name="authenticate">Uygulamayı kullanmak için kimlik doğrulama gerekli</string>
<string name="security">Güvenlik</string>
<string name="enable_biometrics">Biyometrik doğrulamayı etkinleştir</string>
<string name="privacy_mode">Gizlilik modu</string>
<string name="privacy_mode_description">Günlük kaydını ve etkinlik istatistiklerini devre dışı bırakır. Açıldığında mevcut günlükler ve etkinlik geçmişi silinir.</string>
<string name="when_to_ask">Ne zaman sorulsun</string>
<string name="every_time">Her zaman</string>
<string name="one_minute">Bir dakika</string>
@@ -200,6 +200,8 @@
<string name="authenticate">Cần xác thực để sử dụng ứng dụng</string>
<string name="security">Bảo mật</string>
<string name="enable_biometrics">Bật sinh trắc học</string>
<string name="privacy_mode">Chế độ riêng tư</string>
<string name="privacy_mode_description">Tắt ghi nhật ký và thống kê hoạt động. Nhật ký và lịch sử hoạt động hiện có sẽ bị xóa khi bật.</string>
<string name="when_to_ask">Khi nào hỏi</string>
<string name="every_time">Mỗi lần</string>
<string name="one_minute">Một phút</string>
+2
View File
@@ -205,6 +205,8 @@
<string name="authenticate">使用应用需要身份验证</string>
<string name="security">安全性</string>
<string name="enable_biometrics">启用生物识别解锁</string>
<string name="privacy_mode">隐私模式</string>
<string name="privacy_mode_description">禁用日志和活动统计。开启时将删除现有日志和活动历史。</string>
<string name="when_to_ask">解锁请求</string>
<string name="every_time">每次</string>
<string name="one_minute">一分钟</string>
+2
View File
@@ -248,6 +248,8 @@
<string name="authenticate">Authentication needed to use the app</string>
<string name="security">Security</string>
<string name="enable_biometrics">Enable biometrics</string>
<string name="privacy_mode">Privacy mode</string>
<string name="privacy_mode_description">Disables logging and activity statistics. Existing logs and activity history are deleted when turned on.</string>
<string name="when_to_ask">When to ask</string>
<string name="every_time">Every time</string>
<string name="one_minute">One minute</string>