mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:25:19 +00:00
cleanup
This commit is contained in:
@@ -30,6 +30,10 @@ class BitchatApplication : Application() {
|
|||||||
com.bitchat.android.favorites.FavoritesPersistenceService.initialize(this)
|
com.bitchat.android.favorites.FavoritesPersistenceService.initialize(this)
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
|
|
||||||
|
try {
|
||||||
|
com.bitchat.android.storage.StorageModule.registerKnownPanicStores(this)
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
|
||||||
// Warm up Nostr identity to ensure npub is available for favorite notifications
|
// Warm up Nostr identity to ensure npub is available for favorite notifications
|
||||||
try {
|
try {
|
||||||
com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(this)
|
com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(this)
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package com.bitchat.android.favorites
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.bitchat.android.identity.SecureIdentityStateManager
|
import com.bitchat.android.storage.PanicClearRegistry
|
||||||
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
import com.bitchat.android.util.toHexString
|
import com.bitchat.android.util.toHexString
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -82,14 +83,14 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val stateManager = SecureIdentityStateManager(context)
|
private val storage = StorageModule.repository(context, StorageDefinitions.Favorites)
|
||||||
private val gson = Gson()
|
|
||||||
private val favorites = mutableMapOf<String, FavoriteRelationship>() // noiseHex -> relationship
|
private val favorites = mutableMapOf<String, FavoriteRelationship>() // noiseHex -> relationship
|
||||||
// NEW: Index by current mesh peerID (16-hex) for direct lookup when sending Nostr DMs from mesh context
|
// NEW: Index by current mesh peerID (16-hex) for direct lookup when sending Nostr DMs from mesh context
|
||||||
private val peerIdIndex = mutableMapOf<String, String>() // peerID (lowercase 16-hex) -> npub
|
private val peerIdIndex = mutableMapOf<String, String>() // peerID (lowercase 16-hex) -> npub
|
||||||
private val listeners = mutableListOf<FavoritesChangeListener>()
|
private val listeners = mutableListOf<FavoritesChangeListener>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
PanicClearRegistry.register(StorageDefinitions.Favorites) { clearAllFavorites() }
|
||||||
loadFavorites()
|
loadFavorites()
|
||||||
loadPeerIdIndex()
|
loadPeerIdIndex()
|
||||||
}
|
}
|
||||||
@@ -232,9 +233,8 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
|||||||
|
|
||||||
fun clearAllFavorites() {
|
fun clearAllFavorites() {
|
||||||
favorites.clear()
|
favorites.clear()
|
||||||
saveFavorites()
|
|
||||||
peerIdIndex.clear()
|
peerIdIndex.clear()
|
||||||
savePeerIdIndex()
|
storage.clearForPanic()
|
||||||
Log.i(TAG, "Cleared all favorites")
|
Log.i(TAG, "Cleared all favorites")
|
||||||
notifyAllCleared()
|
notifyAllCleared()
|
||||||
}
|
}
|
||||||
@@ -257,11 +257,9 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
|||||||
|
|
||||||
private fun loadFavorites() {
|
private fun loadFavorites() {
|
||||||
try {
|
try {
|
||||||
val favoritesJson = stateManager.getSecureValue(FAVORITES_KEY)
|
val type = object : TypeToken<Map<String, FavoriteRelationshipData>>() {}.type
|
||||||
if (favoritesJson != null) {
|
val data: Map<String, FavoriteRelationshipData>? = storage.getJson(FAVORITES_KEY, type)
|
||||||
val type = object : TypeToken<Map<String, FavoriteRelationshipData>>() {}.type
|
if (data != null) {
|
||||||
val data: Map<String, FavoriteRelationshipData> = gson.fromJson(favoritesJson, type)
|
|
||||||
|
|
||||||
favorites.clear()
|
favorites.clear()
|
||||||
data.forEach { (key, relationshipData) ->
|
data.forEach { (key, relationshipData) ->
|
||||||
favorites[key] = relationshipData.toFavoriteRelationship()
|
favorites[key] = relationshipData.toFavoriteRelationship()
|
||||||
@@ -278,8 +276,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
|||||||
val data = favorites.mapValues { (_, relationship) ->
|
val data = favorites.mapValues { (_, relationship) ->
|
||||||
FavoriteRelationshipData.fromFavoriteRelationship(relationship)
|
FavoriteRelationshipData.fromFavoriteRelationship(relationship)
|
||||||
}
|
}
|
||||||
val favoritesJson = gson.toJson(data)
|
storage.putJson(FAVORITES_KEY, data)
|
||||||
stateManager.storeSecureValue(FAVORITES_KEY, favoritesJson)
|
|
||||||
Log.d(TAG, "Saved ${favorites.size} favorite relationships")
|
Log.d(TAG, "Saved ${favorites.size} favorite relationships")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to save favorites: ${e.message}")
|
Log.e(TAG, "Failed to save favorites: ${e.message}")
|
||||||
@@ -288,10 +285,9 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
|||||||
|
|
||||||
private fun loadPeerIdIndex() {
|
private fun loadPeerIdIndex() {
|
||||||
try {
|
try {
|
||||||
val json = stateManager.getSecureValue(PEERID_INDEX_KEY)
|
val type = object : TypeToken<Map<String, String>>() {}.type
|
||||||
if (json != null) {
|
val data: Map<String, String>? = storage.getJson(PEERID_INDEX_KEY, type)
|
||||||
val type = object : TypeToken<Map<String, String>>() {}.type
|
if (data != null) {
|
||||||
val data: Map<String, String> = gson.fromJson(json, type)
|
|
||||||
peerIdIndex.clear()
|
peerIdIndex.clear()
|
||||||
peerIdIndex.putAll(data)
|
peerIdIndex.putAll(data)
|
||||||
Log.d(TAG, "Loaded ${peerIdIndex.size} peerID→npub mappings")
|
Log.d(TAG, "Loaded ${peerIdIndex.size} peerID→npub mappings")
|
||||||
@@ -303,8 +299,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
|||||||
|
|
||||||
private fun savePeerIdIndex() {
|
private fun savePeerIdIndex() {
|
||||||
try {
|
try {
|
||||||
val json = gson.toJson(peerIdIndex)
|
storage.putJson(PEERID_INDEX_KEY, peerIdIndex)
|
||||||
stateManager.storeSecureValue(PEERID_INDEX_KEY, json)
|
|
||||||
Log.d(TAG, "Saved ${peerIdIndex.size} peerID→npub mappings")
|
Log.d(TAG, "Saved ${peerIdIndex.size} peerID→npub mappings")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to save peerID index: ${e.message}")
|
Log.e(TAG, "Failed to save peerID index: ${e.message}")
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import android.location.Geocoder
|
|||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.bitchat.android.storage.PanicClearRegistry
|
||||||
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -17,7 +20,6 @@ import java.util.Locale
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores a user-maintained list of bookmarked geohash channels.
|
* Stores a user-maintained list of bookmarked geohash channels.
|
||||||
* - Persistence: SharedPreferences (JSON string array)
|
|
||||||
* - Semantics: geohashes are normalized to lowercase base32 and de-duplicated
|
* - Semantics: geohashes are normalized to lowercase base32 and de-duplicated
|
||||||
*/
|
*/
|
||||||
class GeohashBookmarksStore private constructor(private val context: Context) {
|
class GeohashBookmarksStore private constructor(private val context: Context) {
|
||||||
@@ -43,7 +45,7 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
private val prefs = context.getSharedPreferences("geohash_prefs", Context.MODE_PRIVATE)
|
private val storage = StorageModule.repository(context, StorageDefinitions.GeohashBookmarks)
|
||||||
|
|
||||||
private val membership = mutableSetOf<String>()
|
private val membership = mutableSetOf<String>()
|
||||||
|
|
||||||
@@ -56,7 +58,10 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
// For throttling / preventing duplicate geocode lookups
|
// For throttling / preventing duplicate geocode lookups
|
||||||
private val resolving = mutableSetOf<String>()
|
private val resolving = mutableSetOf<String>()
|
||||||
|
|
||||||
init { load() }
|
init {
|
||||||
|
PanicClearRegistry.register(StorageDefinitions.GeohashBookmarks) { clearAll() }
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
fun isBookmarked(geohash: String): Boolean = membership.contains(normalize(geohash))
|
fun isBookmarked(geohash: String): Boolean = membership.contains(normalize(geohash))
|
||||||
|
|
||||||
@@ -95,7 +100,7 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
|
|
||||||
private fun load() {
|
private fun load() {
|
||||||
try {
|
try {
|
||||||
val arrJson = prefs.getString(STORE_KEY, null)
|
val arrJson = storage.getString(STORE_KEY, null)
|
||||||
if (!arrJson.isNullOrEmpty()) {
|
if (!arrJson.isNullOrEmpty()) {
|
||||||
val listType = object : TypeToken<List<String>>() {}.type
|
val listType = object : TypeToken<List<String>>() {}.type
|
||||||
val arr = gson.fromJson<List<String>>(arrJson, listType)
|
val arr = gson.fromJson<List<String>>(arrJson, listType)
|
||||||
@@ -115,7 +120,7 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
Log.e(TAG, "Failed to load bookmarks: ${e.message}")
|
Log.e(TAG, "Failed to load bookmarks: ${e.message}")
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val namesJson = prefs.getString(NAMES_STORE_KEY, null)
|
val namesJson = storage.getString(NAMES_STORE_KEY, null)
|
||||||
if (!namesJson.isNullOrEmpty()) {
|
if (!namesJson.isNullOrEmpty()) {
|
||||||
val mapType = object : TypeToken<Map<String, String>>() {}.type
|
val mapType = object : TypeToken<Map<String, String>>() {}.type
|
||||||
val dict = gson.fromJson<Map<String, String>>(namesJson, mapType)
|
val dict = gson.fromJson<Map<String, String>>(namesJson, mapType)
|
||||||
@@ -129,14 +134,14 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
private fun persist() {
|
private fun persist() {
|
||||||
try {
|
try {
|
||||||
val json = gson.toJson(_bookmarks.value)
|
val json = gson.toJson(_bookmarks.value)
|
||||||
prefs.edit().putString(STORE_KEY, json).apply()
|
storage.putString(STORE_KEY, json)
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persistNames() {
|
private fun persistNames() {
|
||||||
try {
|
try {
|
||||||
val json = gson.toJson(_bookmarkNames.value)
|
val json = gson.toJson(_bookmarkNames.value)
|
||||||
prefs.edit().putString(NAMES_STORE_KEY, json).apply()
|
storage.putString(NAMES_STORE_KEY, json)
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,10 +152,7 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
membership.clear()
|
membership.clear()
|
||||||
_bookmarks.value = emptyList()
|
_bookmarks.value = emptyList()
|
||||||
_bookmarkNames.value = emptyMap()
|
_bookmarkNames.value = emptyMap()
|
||||||
prefs.edit()
|
storage.clearForPanic()
|
||||||
.remove(STORE_KEY)
|
|
||||||
.remove(NAMES_STORE_KEY)
|
|
||||||
.apply()
|
|
||||||
// Clear any in-flight resolutions to avoid repopulating
|
// Clear any in-flight resolutions to avoid repopulating
|
||||||
resolving.clear()
|
resolving.clear()
|
||||||
Log.i(TAG, "Cleared all geohash bookmarks and names")
|
Log.i(TAG, "Cleared all geohash bookmarks and names")
|
||||||
@@ -234,14 +236,14 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
private fun persist(list: List<String>) {
|
private fun persist(list: List<String>) {
|
||||||
try {
|
try {
|
||||||
val json = gson.toJson(list)
|
val json = gson.toJson(list)
|
||||||
prefs.edit().putString(STORE_KEY, json).apply()
|
storage.putString(STORE_KEY, json)
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persistNames(map: Map<String, String>) {
|
private fun persistNames(map: Map<String, String>) {
|
||||||
try {
|
try {
|
||||||
val json = gson.toJson(map)
|
val json = gson.toJson(map)
|
||||||
prefs.edit().putString(NAMES_STORE_KEY, json).apply()
|
storage.putString(NAMES_STORE_KEY, json)
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ class SecureIdentityStateManager(private val context: Context) {
|
|||||||
return prefs.contains(KEY_STATIC_PRIVATE_KEY) && prefs.contains(KEY_STATIC_PUBLIC_KEY)
|
return prefs.contains(KEY_STATIC_PRIVATE_KEY) && prefs.contains(KEY_STATIC_PUBLIC_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public SharedPreferences Access (for favorites and Nostr data)
|
// MARK: - Public Secure Key-Value Access
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a string value in secure preferences
|
* Store a string value in secure preferences
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
package com.bitchat.android.net
|
package com.bitchat.android.net
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
object TorPreferenceManager {
|
object TorPreferenceManager {
|
||||||
private const val PREFS_NAME = "bitchat_settings"
|
|
||||||
private const val KEY_TOR_MODE = "tor_mode"
|
private const val KEY_TOR_MODE = "tor_mode"
|
||||||
|
|
||||||
private val _modeFlow = MutableStateFlow(TorMode.ON)
|
private val _modeFlow = MutableStateFlow(TorMode.ON)
|
||||||
val modeFlow: StateFlow<TorMode> = _modeFlow
|
val modeFlow: StateFlow<TorMode> = _modeFlow
|
||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
val storage = StorageModule.repository(context, StorageDefinitions.AppSettings)
|
||||||
val saved = prefs.getString(KEY_TOR_MODE, TorMode.ON.name)
|
val saved = storage.getString(KEY_TOR_MODE, TorMode.ON.name)
|
||||||
val mode = runCatching { TorMode.valueOf(saved ?: TorMode.ON.name) }.getOrDefault(TorMode.ON)
|
val mode = runCatching { TorMode.valueOf(saved ?: TorMode.ON.name) }.getOrDefault(TorMode.ON)
|
||||||
_modeFlow.value = mode
|
_modeFlow.value = mode
|
||||||
}
|
}
|
||||||
|
|
||||||
fun set(context: Context, mode: TorMode) {
|
fun set(context: Context, mode: TorMode) {
|
||||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
val storage = StorageModule.repository(context, StorageDefinitions.AppSettings)
|
||||||
prefs.edit().putString(KEY_TOR_MODE, mode.name).apply()
|
storage.putString(KEY_TOR_MODE, mode.name)
|
||||||
_modeFlow.value = mode
|
_modeFlow.value = mode
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get(context: Context): TorMode {
|
fun get(context: Context): TorMode {
|
||||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
val storage = StorageModule.repository(context, StorageDefinitions.AppSettings)
|
||||||
val saved = prefs.getString(KEY_TOR_MODE, TorMode.ON.name)
|
val saved = storage.getString(KEY_TOR_MODE, TorMode.ON.name)
|
||||||
return runCatching { TorMode.valueOf(saved ?: TorMode.ON.name) }.getOrDefault(TorMode.ON)
|
return runCatching { TorMode.valueOf(saved ?: TorMode.ON.name) }.getOrDefault(TorMode.ON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,61 @@
|
|||||||
package com.bitchat.android.nostr
|
package com.bitchat.android.nostr
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import com.bitchat.android.storage.PanicClearRegistry
|
||||||
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageMigration
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
|
import com.bitchat.android.storage.StorageRepository
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
|
||||||
* GeohashAliasRegistry
|
|
||||||
* - Global, thread-safe registry for alias->Nostr pubkey mappings (e.g., nostr_<pub16> -> pubkeyHex)
|
|
||||||
* - Persisted to SharedPreferences to survive app restarts.
|
|
||||||
*/
|
|
||||||
object GeohashAliasRegistry {
|
object GeohashAliasRegistry {
|
||||||
private val map: MutableMap<String, String> = ConcurrentHashMap()
|
private val map: MutableMap<String, String> = ConcurrentHashMap()
|
||||||
private const val PREFS_NAME = "geohash_alias_registry"
|
private const val PREFS_NAME = "geohash_alias_registry"
|
||||||
private var prefs: SharedPreferences? = null
|
private const val ENTRIES_KEY = "entries_v1"
|
||||||
|
private var storage: StorageRepository? = null
|
||||||
|
|
||||||
fun initialize(context: Context) {
|
fun initialize(context: Context) {
|
||||||
if (prefs == null) {
|
if (storage == null) {
|
||||||
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
val legacyPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
loadFromPrefs()
|
storage = StorageModule.repository(
|
||||||
|
context = context,
|
||||||
|
definition = StorageDefinitions.GeohashAliasRegistry,
|
||||||
|
migrations = listOf(
|
||||||
|
StorageMigration(toVersion = 1) { repository ->
|
||||||
|
if (!repository.contains(ENTRIES_KEY)) {
|
||||||
|
val migrated = legacyPrefs.all
|
||||||
|
.mapNotNull { (key, value) -> if (value is String) key to value else null }
|
||||||
|
.toMap()
|
||||||
|
if (migrated.isNotEmpty()) {
|
||||||
|
repository.putJson(ENTRIES_KEY, migrated)
|
||||||
|
migrated.keys.forEach { repository.remove(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
PanicClearRegistry.register(StorageDefinitions.GeohashAliasRegistry) { clear() }
|
||||||
|
loadFromStorage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFromPrefs() {
|
private fun loadFromStorage() {
|
||||||
prefs?.let { p ->
|
val repository = storage ?: return
|
||||||
val allEntries = p.all
|
val type = object : TypeToken<Map<String, String>>() {}.type
|
||||||
for ((key, value) in allEntries) {
|
repository.getJson<Map<String, String>>(ENTRIES_KEY, type)?.let { entries ->
|
||||||
if (key is String && value is String) {
|
map.clear()
|
||||||
map[key] = value
|
map.putAll(entries)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun persist() {
|
||||||
|
storage?.putJson(ENTRIES_KEY, map.toMap())
|
||||||
|
}
|
||||||
|
|
||||||
fun put(alias: String, pubkeyHex: String) {
|
fun put(alias: String, pubkeyHex: String) {
|
||||||
map[alias] = pubkeyHex
|
map[alias] = pubkeyHex
|
||||||
prefs?.edit()?.putString(alias, pubkeyHex)?.apply()
|
persist()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get(alias: String): String? = map[alias]
|
fun get(alias: String): String? = map[alias]
|
||||||
@@ -45,6 +66,6 @@ object GeohashAliasRegistry {
|
|||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
map.clear()
|
map.clear()
|
||||||
prefs?.edit()?.clear()?.apply()
|
storage?.clearForPanic()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,62 @@
|
|||||||
package com.bitchat.android.nostr
|
package com.bitchat.android.nostr
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import com.bitchat.android.storage.PanicClearRegistry
|
||||||
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageMigration
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
|
import com.bitchat.android.storage.StorageRepository
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
|
||||||
* GeohashConversationRegistry
|
|
||||||
* - Global, thread-safe registry of conversationKey (e.g., "nostr_<pub16>") -> source geohash
|
|
||||||
* - Enables routing geohash DMs from anywhere by providing the correct geohash identity
|
|
||||||
* - Persisted to SharedPreferences to survive app restarts.
|
|
||||||
*/
|
|
||||||
object GeohashConversationRegistry {
|
object GeohashConversationRegistry {
|
||||||
private val map = ConcurrentHashMap<String, String>()
|
private val map = ConcurrentHashMap<String, String>()
|
||||||
private const val PREFS_NAME = "geohash_conversation_registry"
|
private const val PREFS_NAME = "geohash_conversation_registry"
|
||||||
private var prefs: SharedPreferences? = null
|
private const val ENTRIES_KEY = "entries_v1"
|
||||||
|
private var storage: StorageRepository? = null
|
||||||
|
|
||||||
fun initialize(context: Context) {
|
fun initialize(context: Context) {
|
||||||
if (prefs == null) {
|
if (storage == null) {
|
||||||
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
val legacyPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
loadFromPrefs()
|
storage = StorageModule.repository(
|
||||||
|
context = context,
|
||||||
|
definition = StorageDefinitions.GeohashConversationRegistry,
|
||||||
|
migrations = listOf(
|
||||||
|
StorageMigration(toVersion = 1) { repository ->
|
||||||
|
if (!repository.contains(ENTRIES_KEY)) {
|
||||||
|
val migrated = legacyPrefs.all
|
||||||
|
.mapNotNull { (key, value) -> if (value is String) key to value else null }
|
||||||
|
.toMap()
|
||||||
|
if (migrated.isNotEmpty()) {
|
||||||
|
repository.putJson(ENTRIES_KEY, migrated)
|
||||||
|
migrated.keys.forEach { repository.remove(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
PanicClearRegistry.register(StorageDefinitions.GeohashConversationRegistry) { clear() }
|
||||||
|
loadFromStorage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFromPrefs() {
|
private fun loadFromStorage() {
|
||||||
prefs?.let { p ->
|
val repository = storage ?: return
|
||||||
val allEntries = p.all
|
val type = object : TypeToken<Map<String, String>>() {}.type
|
||||||
for ((key, value) in allEntries) {
|
repository.getJson<Map<String, String>>(ENTRIES_KEY, type)?.let { entries ->
|
||||||
if (key is String && value is String) {
|
map.clear()
|
||||||
map[key] = value
|
map.putAll(entries)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun persist() {
|
||||||
|
storage?.putJson(ENTRIES_KEY, map.toMap())
|
||||||
|
}
|
||||||
|
|
||||||
fun set(convKey: String, geohash: String) {
|
fun set(convKey: String, geohash: String) {
|
||||||
if (geohash.isNotEmpty()) {
|
if (geohash.isNotEmpty()) {
|
||||||
map[convKey] = geohash
|
map[convKey] = geohash
|
||||||
prefs?.edit()?.putString(convKey, geohash)?.apply()
|
persist()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +66,6 @@ object GeohashConversationRegistry {
|
|||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
map.clear()
|
map.clear()
|
||||||
prefs?.edit()?.clear()?.apply()
|
storage?.clearForPanic()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.bitchat.android.nostr
|
package com.bitchat.android.nostr
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
|
import com.bitchat.android.storage.StorageRepository
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -11,7 +13,6 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
*/
|
*/
|
||||||
object PoWPreferenceManager {
|
object PoWPreferenceManager {
|
||||||
|
|
||||||
private const val PREFS_NAME = "pow_preferences"
|
|
||||||
private const val KEY_POW_ENABLED = "pow_enabled"
|
private const val KEY_POW_ENABLED = "pow_enabled"
|
||||||
private const val KEY_POW_DIFFICULTY = "pow_difficulty"
|
private const val KEY_POW_DIFFICULTY = "pow_difficulty"
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ object PoWPreferenceManager {
|
|||||||
private val _isMining = MutableStateFlow(false)
|
private val _isMining = MutableStateFlow(false)
|
||||||
val isMining: StateFlow<Boolean> = _isMining.asStateFlow()
|
val isMining: StateFlow<Boolean> = _isMining.asStateFlow()
|
||||||
|
|
||||||
private lateinit var sharedPrefs: SharedPreferences
|
private lateinit var storage: StorageRepository
|
||||||
private var isInitialized = false
|
private var isInitialized = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,11 +41,11 @@ object PoWPreferenceManager {
|
|||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
if (isInitialized) return
|
if (isInitialized) return
|
||||||
|
|
||||||
sharedPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
storage = StorageModule.repository(context, StorageDefinitions.PowPreferences)
|
||||||
|
|
||||||
// Load current values
|
// Load current values
|
||||||
_powEnabled.value = sharedPrefs.getBoolean(KEY_POW_ENABLED, DEFAULT_POW_ENABLED)
|
_powEnabled.value = storage.getBoolean(KEY_POW_ENABLED, DEFAULT_POW_ENABLED)
|
||||||
_powDifficulty.value = sharedPrefs.getInt(KEY_POW_DIFFICULTY, DEFAULT_POW_DIFFICULTY)
|
_powDifficulty.value = storage.getInt(KEY_POW_DIFFICULTY, DEFAULT_POW_DIFFICULTY)
|
||||||
|
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
}
|
}
|
||||||
@@ -61,8 +62,8 @@ object PoWPreferenceManager {
|
|||||||
*/
|
*/
|
||||||
fun setPowEnabled(enabled: Boolean) {
|
fun setPowEnabled(enabled: Boolean) {
|
||||||
_powEnabled.value = enabled
|
_powEnabled.value = enabled
|
||||||
if (::sharedPrefs.isInitialized) {
|
if (::storage.isInitialized) {
|
||||||
sharedPrefs.edit().putBoolean(KEY_POW_ENABLED, enabled).apply()
|
storage.putBoolean(KEY_POW_ENABLED, enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +80,8 @@ object PoWPreferenceManager {
|
|||||||
fun setPowDifficulty(difficulty: Int) {
|
fun setPowDifficulty(difficulty: Int) {
|
||||||
val clampedDifficulty = difficulty.coerceIn(0, 32)
|
val clampedDifficulty = difficulty.coerceIn(0, 32)
|
||||||
_powDifficulty.value = clampedDifficulty
|
_powDifficulty.value = clampedDifficulty
|
||||||
if (::sharedPrefs.isInitialized) {
|
if (::storage.isInitialized) {
|
||||||
sharedPrefs.edit().putInt(KEY_POW_DIFFICULTY, clampedDifficulty).apply()
|
storage.putInt(KEY_POW_DIFFICULTY, clampedDifficulty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-8
@@ -1,21 +1,19 @@
|
|||||||
package com.bitchat.android.onboarding
|
package com.bitchat.android.onboarding
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
|
|
||||||
/**
|
|
||||||
* Preference manager for background location skip choice.
|
|
||||||
*/
|
|
||||||
object BackgroundLocationPreferenceManager {
|
object BackgroundLocationPreferenceManager {
|
||||||
private const val PREFS_NAME = "bitchat_settings"
|
|
||||||
private const val KEY_BACKGROUND_LOCATION_SKIP = "background_location_skipped"
|
private const val KEY_BACKGROUND_LOCATION_SKIP = "background_location_skipped"
|
||||||
|
|
||||||
fun setSkipped(context: Context, skipped: Boolean) {
|
fun setSkipped(context: Context, skipped: Boolean) {
|
||||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
val storage = StorageModule.repository(context, StorageDefinitions.AppSettings)
|
||||||
prefs.edit().putBoolean(KEY_BACKGROUND_LOCATION_SKIP, skipped).apply()
|
storage.putBoolean(KEY_BACKGROUND_LOCATION_SKIP, skipped)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isSkipped(context: Context): Boolean {
|
fun isSkipped(context: Context): Boolean {
|
||||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
val storage = StorageModule.repository(context, StorageDefinitions.AppSettings)
|
||||||
return prefs.getBoolean(KEY_BACKGROUND_LOCATION_SKIP, false)
|
return storage.getBoolean(KEY_BACKGROUND_LOCATION_SKIP, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-10
@@ -1,33 +1,31 @@
|
|||||||
package com.bitchat.android.onboarding
|
package com.bitchat.android.onboarding
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
/**
|
|
||||||
* Preference manager for battery optimization skip choice
|
|
||||||
*/
|
|
||||||
object BatteryOptimizationPreferenceManager {
|
object BatteryOptimizationPreferenceManager {
|
||||||
private const val PREFS_NAME = "bitchat_settings"
|
|
||||||
private const val KEY_BATTERY_SKIP = "battery_optimization_skipped"
|
private const val KEY_BATTERY_SKIP = "battery_optimization_skipped"
|
||||||
|
|
||||||
private val _skipFlow = MutableStateFlow(false)
|
private val _skipFlow = MutableStateFlow(false)
|
||||||
val skipFlow: StateFlow<Boolean> = _skipFlow
|
val skipFlow: StateFlow<Boolean> = _skipFlow
|
||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
val storage = StorageModule.repository(context, StorageDefinitions.AppSettings)
|
||||||
val skipped = prefs.getBoolean(KEY_BATTERY_SKIP, false)
|
val skipped = storage.getBoolean(KEY_BATTERY_SKIP, false)
|
||||||
_skipFlow.value = skipped
|
_skipFlow.value = skipped
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSkipped(context: Context, skipped: Boolean) {
|
fun setSkipped(context: Context, skipped: Boolean) {
|
||||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
val storage = StorageModule.repository(context, StorageDefinitions.AppSettings)
|
||||||
prefs.edit().putBoolean(KEY_BATTERY_SKIP, skipped).apply()
|
storage.putBoolean(KEY_BATTERY_SKIP, skipped)
|
||||||
_skipFlow.value = skipped
|
_skipFlow.value = skipped
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isSkipped(context: Context): Boolean {
|
fun isSkipped(context: Context): Boolean {
|
||||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
val storage = StorageModule.repository(context, StorageDefinitions.AppSettings)
|
||||||
return prefs.getBoolean(KEY_BATTERY_SKIP, false)
|
return storage.getBoolean(KEY_BATTERY_SKIP, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import android.os.PowerManager
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.bitchat.android.R
|
import com.bitchat.android.R
|
||||||
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Centralized permission management for bitchat app
|
* Centralized permission management for bitchat app
|
||||||
@@ -17,26 +19,23 @@ class PermissionManager(private val context: Context) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PermissionManager"
|
private const val TAG = "PermissionManager"
|
||||||
private const val PREFS_NAME = "bitchat_permissions"
|
|
||||||
private const val KEY_FIRST_TIME_COMPLETE = "first_time_onboarding_complete"
|
private const val KEY_FIRST_TIME_COMPLETE = "first_time_onboarding_complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val sharedPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
private val storage = StorageModule.repository(context, StorageDefinitions.OnboardingPermissions)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if this is the first time the user is launching the app
|
* Check if this is the first time the user is launching the app
|
||||||
*/
|
*/
|
||||||
fun isFirstTimeLaunch(): Boolean {
|
fun isFirstTimeLaunch(): Boolean {
|
||||||
return !sharedPrefs.getBoolean(KEY_FIRST_TIME_COMPLETE, false)
|
return !storage.getBoolean(KEY_FIRST_TIME_COMPLETE, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark the first-time onboarding as complete
|
* Mark the first-time onboarding as complete
|
||||||
*/
|
*/
|
||||||
fun markOnboardingComplete() {
|
fun markOnboardingComplete() {
|
||||||
sharedPrefs.edit()
|
storage.putBoolean(KEY_FIRST_TIME_COMPLETE, true)
|
||||||
.putBoolean(KEY_FIRST_TIME_COMPLETE, true)
|
|
||||||
.apply()
|
|
||||||
Log.d(TAG, "First-time onboarding marked as complete")
|
Log.d(TAG, "First-time onboarding marked as complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,14 +125,20 @@ class MeshForegroundService : Service() {
|
|||||||
notificationManager = NotificationManagerCompat.from(this)
|
notificationManager = NotificationManagerCompat.from(this)
|
||||||
createChannel()
|
createChannel()
|
||||||
|
|
||||||
// Ensure mesh service exists in holder (create if needed)
|
// Avoid creating or holding a mesh instance unless this service can own
|
||||||
|
// the foreground/background lifecycle.
|
||||||
val existing = MeshServiceHolder.meshService
|
val existing = MeshServiceHolder.meshService
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
Log.d("MeshForegroundService", "Using existing BluetoothMeshService from holder")
|
Log.d("MeshForegroundService", "Using existing BluetoothMeshService from holder")
|
||||||
} else {
|
} else if (isForegroundMeshEligible()) {
|
||||||
val created = MeshServiceHolder.getOrCreate(applicationContext)
|
val created = MeshServiceHolder.getOrCreate(applicationContext)
|
||||||
Log.i("MeshForegroundService", "Created new BluetoothMeshService via holder")
|
Log.i("MeshForegroundService", "Created new BluetoothMeshService via holder")
|
||||||
MeshServiceHolder.attach(created)
|
MeshServiceHolder.attach(created)
|
||||||
|
} else {
|
||||||
|
Log.i(
|
||||||
|
"MeshForegroundService",
|
||||||
|
"Mesh not created; foreground mesh is not eligible (background=${isBackgroundMeshEnabled()}, notification=${hasNotificationPermission()}, bluetooth=${hasBluetoothPermissions()})"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,11 +153,10 @@ class MeshForegroundService : Service() {
|
|||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
ACTION_STOP -> {
|
ACTION_STOP -> {
|
||||||
// Stop FGS and mesh cleanly
|
// Stop FGS and mesh cleanly
|
||||||
try { meshService?.stopServices() } catch (_: Exception) { }
|
updateJob?.cancel()
|
||||||
try { MeshServiceHolder.clear() } catch (_: Exception) { }
|
updateJob = null
|
||||||
try { stopForeground(true) } catch (_: Exception) { }
|
ensureMeshStopped("stop requested")
|
||||||
notificationManager.cancel(NOTIFICATION_ID)
|
clearForegroundNotification(removeNotification = true)
|
||||||
isInForeground = false
|
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
@@ -177,7 +182,7 @@ class MeshForegroundService : Service() {
|
|||||||
}
|
}
|
||||||
ACTION_UPDATE_NOTIFICATION -> {
|
ACTION_UPDATE_NOTIFICATION -> {
|
||||||
// If we became eligible and are not in foreground yet, promote once
|
// If we became eligible and are not in foreground yet, promote once
|
||||||
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions() && !isInForeground) {
|
if (isForegroundMeshEligible() && !isInForeground) {
|
||||||
val n = buildNotification(meshService?.getActivePeerCount() ?: 0)
|
val n = buildNotification(meshService?.getActivePeerCount() ?: 0)
|
||||||
startForegroundCompat(n)
|
startForegroundCompat(n)
|
||||||
isInForeground = true
|
isInForeground = true
|
||||||
@@ -188,11 +193,18 @@ class MeshForegroundService : Service() {
|
|||||||
else -> { /* ACTION_START or null */ }
|
else -> { /* ACTION_START or null */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isForegroundMeshEligible()) {
|
||||||
|
ensureMeshStopped("foreground mesh no longer eligible")
|
||||||
|
clearForegroundNotification(removeNotification = true)
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure mesh is running (only after permissions are granted)
|
// Ensure mesh is running (only after permissions are granted)
|
||||||
ensureMeshStarted()
|
ensureMeshStarted()
|
||||||
|
|
||||||
// Promote exactly once when eligible, otherwise stay background (or stop)
|
// Promote exactly once when eligible, otherwise stay background (or stop)
|
||||||
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions() && !isInForeground) {
|
if (isForegroundMeshEligible() && !isInForeground) {
|
||||||
val notification = buildNotification(meshService?.getActivePeerCount() ?: 0)
|
val notification = buildNotification(meshService?.getActivePeerCount() ?: 0)
|
||||||
startForegroundCompat(notification)
|
startForegroundCompat(notification)
|
||||||
isInForeground = true
|
isInForeground = true
|
||||||
@@ -203,18 +215,16 @@ class MeshForegroundService : Service() {
|
|||||||
updateJob = scope.launch {
|
updateJob = scope.launch {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
// Retry enabling mesh/foreground once permissions become available
|
// Retry enabling mesh/foreground once permissions become available
|
||||||
ensureMeshStarted()
|
val eligible = isForegroundMeshEligible()
|
||||||
val eligible = MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions()
|
|
||||||
if (eligible) {
|
if (eligible) {
|
||||||
|
ensureMeshStarted()
|
||||||
// Only update the notification; do not re-call startForeground()
|
// Only update the notification; do not re-call startForeground()
|
||||||
updateNotification(force = false)
|
updateNotification(force = false)
|
||||||
} else {
|
} else {
|
||||||
// If disabled or perms missing, ensure we are not in foreground and clear notif
|
ensureMeshStopped("foreground mesh eligibility lost")
|
||||||
if (isInForeground) {
|
clearForegroundNotification(removeNotification = true)
|
||||||
try { stopForeground(false) } catch (_: Exception) { }
|
stopSelf()
|
||||||
isInForeground = false
|
break
|
||||||
}
|
|
||||||
notificationManager.cancel(NOTIFICATION_ID)
|
|
||||||
}
|
}
|
||||||
delay(5000)
|
delay(5000)
|
||||||
}
|
}
|
||||||
@@ -226,7 +236,10 @@ class MeshForegroundService : Service() {
|
|||||||
|
|
||||||
private fun ensureMeshStarted() {
|
private fun ensureMeshStarted() {
|
||||||
if (isShuttingDown) return
|
if (isShuttingDown) return
|
||||||
if (!hasBluetoothPermissions()) return
|
if (!isForegroundMeshEligible()) {
|
||||||
|
ensureMeshStopped("foreground mesh not eligible")
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
android.util.Log.d("MeshForegroundService", "Ensuring mesh service is started")
|
android.util.Log.d("MeshForegroundService", "Ensuring mesh service is started")
|
||||||
val service = MeshServiceHolder.getOrCreate(applicationContext)
|
val service = MeshServiceHolder.getOrCreate(applicationContext)
|
||||||
@@ -236,6 +249,22 @@ class MeshForegroundService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ensureMeshStopped(reason: String) {
|
||||||
|
val service = meshService ?: return
|
||||||
|
try {
|
||||||
|
android.util.Log.i("MeshForegroundService", "Stopping mesh service: $reason")
|
||||||
|
service.stopServices()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.w("MeshForegroundService", "Failed to stop mesh service: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (MeshServiceHolder.meshService === service) {
|
||||||
|
MeshServiceHolder.clear()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateNotification(force: Boolean) {
|
private fun updateNotification(force: Boolean) {
|
||||||
if (isShuttingDown) {
|
if (isShuttingDown) {
|
||||||
notificationManager.cancel(NOTIFICATION_ID)
|
notificationManager.cancel(NOTIFICATION_ID)
|
||||||
@@ -243,16 +272,23 @@ class MeshForegroundService : Service() {
|
|||||||
}
|
}
|
||||||
val count = meshService?.getActivePeerCount() ?: 0
|
val count = meshService?.getActivePeerCount() ?: 0
|
||||||
val notification = buildNotification(count)
|
val notification = buildNotification(count)
|
||||||
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions()) {
|
if (isForegroundMeshEligible()) {
|
||||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||||
} else if (force) {
|
} else if (force) {
|
||||||
// If disabled and forced, make sure to remove any prior foreground state
|
// If disabled and forced, make sure to remove any prior foreground state
|
||||||
try { stopForeground(false) } catch (_: Exception) { }
|
ensureMeshStopped("notification update while ineligible")
|
||||||
notificationManager.cancel(NOTIFICATION_ID)
|
clearForegroundNotification(removeNotification = true)
|
||||||
isInForeground = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isForegroundMeshEligible(): Boolean {
|
||||||
|
return isBackgroundMeshEnabled() && hasAllRequiredPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isBackgroundMeshEnabled(): Boolean {
|
||||||
|
return MeshServicePreferences.isBackgroundEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
private fun hasAllRequiredPermissions(): Boolean {
|
private fun hasAllRequiredPermissions(): Boolean {
|
||||||
// For starting FGS with connectedDevice|dataSync, we need:
|
// For starting FGS with connectedDevice|dataSync, we need:
|
||||||
// - Foreground service permissions (declared in manifest)
|
// - Foreground service permissions (declared in manifest)
|
||||||
@@ -280,6 +316,16 @@ class MeshForegroundService : Service() {
|
|||||||
} else true
|
} else true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun clearForegroundNotification(removeNotification: Boolean) {
|
||||||
|
if (isInForeground) {
|
||||||
|
try { stopForeground(removeNotification) } catch (_: Exception) { }
|
||||||
|
isInForeground = false
|
||||||
|
}
|
||||||
|
if (removeNotification) {
|
||||||
|
notificationManager.cancel(NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildNotification(activePeers: Int): Notification {
|
private fun buildNotification(activePeers: Int): Notification {
|
||||||
val openIntent = Intent(this, MainActivity::class.java)
|
val openIntent = Intent(this, MainActivity::class.java)
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
@@ -362,13 +408,13 @@ class MeshForegroundService : Service() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
updateJob?.cancel()
|
updateJob?.cancel()
|
||||||
updateJob = null
|
updateJob = null
|
||||||
|
if (isShuttingDown || !isForegroundMeshEligible()) {
|
||||||
|
ensureMeshStopped("service destroyed")
|
||||||
|
}
|
||||||
// Cancel the service coroutine scope to prevent leaks
|
// Cancel the service coroutine scope to prevent leaks
|
||||||
try { serviceJob.cancel() } catch (_: Exception) { }
|
try { serviceJob.cancel() } catch (_: Exception) { }
|
||||||
// Best-effort ensure we are not marked foreground
|
// Best-effort ensure we are not marked foreground
|
||||||
if (isInForeground) {
|
clearForegroundNotification(removeNotification = true)
|
||||||
try { stopForeground(true) } catch (_: Exception) { }
|
|
||||||
isInForeground = false
|
|
||||||
}
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
package com.bitchat.android.service
|
package com.bitchat.android.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
|
import com.bitchat.android.storage.StorageRepository
|
||||||
|
|
||||||
object MeshServicePreferences {
|
object MeshServicePreferences {
|
||||||
private const val PREFS_NAME = "bitchat_mesh_service_prefs"
|
|
||||||
private const val KEY_AUTO_START = "auto_start_on_boot"
|
private const val KEY_AUTO_START = "auto_start_on_boot"
|
||||||
private const val KEY_BACKGROUND_ENABLED = "background_enabled"
|
private const val KEY_BACKGROUND_ENABLED = "background_enabled"
|
||||||
|
|
||||||
private lateinit var prefs: SharedPreferences
|
private lateinit var storage: StorageRepository
|
||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
storage = StorageModule.repository(context, StorageDefinitions.MeshServicePreferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ready(): Boolean = ::storage.isInitialized
|
||||||
|
|
||||||
fun isAutoStartEnabled(default: Boolean = true): Boolean {
|
fun isAutoStartEnabled(default: Boolean = true): Boolean {
|
||||||
return prefs.getBoolean(KEY_AUTO_START, default)
|
return if (ready()) storage.getBoolean(KEY_AUTO_START, default) else default
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAutoStartEnabled(enabled: Boolean) {
|
fun setAutoStartEnabled(enabled: Boolean) {
|
||||||
prefs.edit().putBoolean(KEY_AUTO_START, enabled).apply()
|
if (ready()) storage.putBoolean(KEY_AUTO_START, enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isBackgroundEnabled(default: Boolean = true): Boolean {
|
fun isBackgroundEnabled(default: Boolean = true): Boolean {
|
||||||
return prefs.getBoolean(KEY_BACKGROUND_ENABLED, default)
|
return if (ready()) storage.getBoolean(KEY_BACKGROUND_ENABLED, default) else default
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setBackgroundEnabled(enabled: Boolean) {
|
fun setBackgroundEnabled(enabled: Boolean) {
|
||||||
prefs.edit().putBoolean(KEY_BACKGROUND_ENABLED, enabled).apply()
|
if (ready()) storage.putBoolean(KEY_BACKGROUND_ENABLED, enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package com.bitchat.android.services
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.bitchat.android.identity.SecureIdentityStateManager
|
import com.bitchat.android.storage.PanicClearRegistry
|
||||||
import com.google.gson.Gson
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persistent store for message IDs we've already acknowledged (DELIVERED) or READ.
|
* Persistent store for message IDs we've already acknowledged (DELIVERED) or READ.
|
||||||
@@ -23,13 +24,15 @@ class SeenMessageStore private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val gson = Gson()
|
private val storage = StorageModule.repository(context, StorageDefinitions.SeenMessages)
|
||||||
private val secure = SecureIdentityStateManager(context)
|
|
||||||
|
|
||||||
private val delivered = LinkedHashSet<String>(MAX_IDS)
|
private val delivered = LinkedHashSet<String>(MAX_IDS)
|
||||||
private val read = LinkedHashSet<String>(MAX_IDS)
|
private val read = LinkedHashSet<String>(MAX_IDS)
|
||||||
|
|
||||||
init { load() }
|
init {
|
||||||
|
PanicClearRegistry.register(StorageDefinitions.SeenMessages) { clear() }
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized fun hasDelivered(id: String) = delivered.contains(id)
|
@Synchronized fun hasDelivered(id: String) = delivered.contains(id)
|
||||||
@Synchronized fun hasRead(id: String) = read.contains(id)
|
@Synchronized fun hasRead(id: String) = read.contains(id)
|
||||||
@@ -53,7 +56,7 @@ class SeenMessageStore private constructor(private val context: Context) {
|
|||||||
@Synchronized fun clear() {
|
@Synchronized fun clear() {
|
||||||
delivered.clear()
|
delivered.clear()
|
||||||
read.clear()
|
read.clear()
|
||||||
persist()
|
storage.clearForPanic()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun trim(set: LinkedHashSet<String>) {
|
private fun trim(set: LinkedHashSet<String>) {
|
||||||
@@ -66,8 +69,7 @@ class SeenMessageStore private constructor(private val context: Context) {
|
|||||||
|
|
||||||
@Synchronized private fun load() {
|
@Synchronized private fun load() {
|
||||||
try {
|
try {
|
||||||
val json = secure.getSecureValue(STORAGE_KEY) ?: return
|
val data = storage.getJson(STORAGE_KEY, StorePayload::class.java) ?: return
|
||||||
val data = gson.fromJson(json, StorePayload::class.java) ?: return
|
|
||||||
delivered.clear(); read.clear()
|
delivered.clear(); read.clear()
|
||||||
data.delivered.takeLast(MAX_IDS).forEach { delivered.add(it) }
|
data.delivered.takeLast(MAX_IDS).forEach { delivered.add(it) }
|
||||||
data.read.takeLast(MAX_IDS).forEach { read.add(it) }
|
data.read.takeLast(MAX_IDS).forEach { read.add(it) }
|
||||||
@@ -80,8 +82,7 @@ class SeenMessageStore private constructor(private val context: Context) {
|
|||||||
@Synchronized private fun persist() {
|
@Synchronized private fun persist() {
|
||||||
try {
|
try {
|
||||||
val payload = StorePayload(delivered.toList(), read.toList())
|
val payload = StorePayload(delivered.toList(), read.toList())
|
||||||
val json = gson.toJson(payload)
|
storage.putJson(STORAGE_KEY, payload)
|
||||||
secure.storeSecureValue(STORAGE_KEY, json)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to persist SeenMessageStore: ${e.message}")
|
Log.e(TAG, "Failed to persist SeenMessageStore: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.bitchat.android.storage
|
||||||
|
|
||||||
|
interface KeyValueStore {
|
||||||
|
fun getString(key: String, defaultValue: String? = null): String?
|
||||||
|
fun putString(key: String, value: String?)
|
||||||
|
fun getBoolean(key: String, defaultValue: Boolean): Boolean
|
||||||
|
fun putBoolean(key: String, value: Boolean)
|
||||||
|
fun getInt(key: String, defaultValue: Int): Int
|
||||||
|
fun putInt(key: String, value: Int)
|
||||||
|
fun getLong(key: String, defaultValue: Long): Long
|
||||||
|
fun putLong(key: String, value: Long)
|
||||||
|
fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Set<String>
|
||||||
|
fun putStringSet(key: String, value: Set<String>)
|
||||||
|
fun contains(key: String): Boolean
|
||||||
|
fun remove(key: String)
|
||||||
|
fun clear()
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.bitchat.android.storage
|
||||||
|
|
||||||
|
data class PanicClearEntry(
|
||||||
|
val definition: StorageDefinition,
|
||||||
|
val clear: () -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PanicClearResult(
|
||||||
|
val id: String,
|
||||||
|
val owner: String,
|
||||||
|
val success: Boolean,
|
||||||
|
val errorMessage: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
object PanicClearRegistry {
|
||||||
|
private val entries = linkedMapOf<String, PanicClearEntry>()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun register(definition: StorageDefinition, clear: () -> Unit) {
|
||||||
|
if (definition.panicClearPolicy == PanicClearPolicy.CLEAR_ON_PANIC) {
|
||||||
|
entries[definition.id] = PanicClearEntry(definition, clear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun registerIfAbsent(definition: StorageDefinition, clear: () -> Unit) {
|
||||||
|
if (definition.panicClearPolicy == PanicClearPolicy.CLEAR_ON_PANIC && !entries.containsKey(definition.id)) {
|
||||||
|
entries[definition.id] = PanicClearEntry(definition, clear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun registeredEntries(): List<PanicClearEntry> = entries.values.toList()
|
||||||
|
|
||||||
|
fun clearAll(): List<PanicClearResult> {
|
||||||
|
val snapshot = synchronized(this) { entries.values.toList() }
|
||||||
|
return snapshot.map { entry ->
|
||||||
|
try {
|
||||||
|
entry.clear()
|
||||||
|
PanicClearResult(
|
||||||
|
id = entry.definition.id,
|
||||||
|
owner = entry.definition.owner,
|
||||||
|
success = true
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
PanicClearResult(
|
||||||
|
id = entry.definition.id,
|
||||||
|
owner = entry.definition.owner,
|
||||||
|
success = false,
|
||||||
|
errorMessage = e.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun resetForTesting() {
|
||||||
|
entries.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.bitchat.android.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.bitchat.android.identity.SecureIdentityStateManager
|
||||||
|
|
||||||
|
class SecureIdentityKeyValueStore(
|
||||||
|
context: Context
|
||||||
|
) : KeyValueStore {
|
||||||
|
|
||||||
|
private val secure = SecureIdentityStateManager(context.applicationContext)
|
||||||
|
|
||||||
|
override fun getString(key: String, defaultValue: String?): String? {
|
||||||
|
return secure.getSecureValue(key) ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putString(key: String, value: String?) {
|
||||||
|
if (value == null) {
|
||||||
|
secure.removeSecureValue(key)
|
||||||
|
} else {
|
||||||
|
secure.storeSecureValue(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBoolean(key: String, defaultValue: Boolean): Boolean {
|
||||||
|
return getString(key)?.toBooleanStrictOrNull() ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putBoolean(key: String, value: Boolean) {
|
||||||
|
putString(key, value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInt(key: String, defaultValue: Int): Int {
|
||||||
|
return getString(key)?.toIntOrNull() ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putInt(key: String, value: Int) {
|
||||||
|
putString(key, value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLong(key: String, defaultValue: Long): Long {
|
||||||
|
return getString(key)?.toLongOrNull() ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putLong(key: String, value: Long) {
|
||||||
|
putString(key, value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStringSet(key: String, defaultValue: Set<String>): Set<String> {
|
||||||
|
return getString(key)?.split(STRING_SET_SEPARATOR)?.filter { it.isNotEmpty() }?.toSet()
|
||||||
|
?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putStringSet(key: String, value: Set<String>) {
|
||||||
|
putString(key, value.joinToString(STRING_SET_SEPARATOR))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contains(key: String): Boolean {
|
||||||
|
return secure.hasSecureValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(key: String) {
|
||||||
|
secure.removeSecureValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear() {
|
||||||
|
secure.clearIdentityData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val STRING_SET_SEPARATOR = "\u001F"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.bitchat.android.storage
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
|
class SharedPreferencesKeyValueStore(
|
||||||
|
private val prefs: SharedPreferences
|
||||||
|
) : KeyValueStore {
|
||||||
|
|
||||||
|
override fun getString(key: String, defaultValue: String?): String? {
|
||||||
|
return prefs.getString(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putString(key: String, value: String?) {
|
||||||
|
prefs.edit { if (value == null) remove(key) else putString(key, value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBoolean(key: String, defaultValue: Boolean): Boolean {
|
||||||
|
return prefs.getBoolean(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putBoolean(key: String, value: Boolean) {
|
||||||
|
prefs.edit { putBoolean(key, value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInt(key: String, defaultValue: Int): Int {
|
||||||
|
return prefs.getInt(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putInt(key: String, value: Int) {
|
||||||
|
prefs.edit { putInt(key, value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLong(key: String, defaultValue: Long): Long {
|
||||||
|
return prefs.getLong(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putLong(key: String, value: Long) {
|
||||||
|
prefs.edit { putLong(key, value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStringSet(key: String, defaultValue: Set<String>): Set<String> {
|
||||||
|
return prefs.getStringSet(key, defaultValue)?.toSet() ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putStringSet(key: String, value: Set<String>) {
|
||||||
|
prefs.edit { putStringSet(key, value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contains(key: String): Boolean {
|
||||||
|
return prefs.contains(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(key: String) {
|
||||||
|
prefs.edit { remove(key) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear() {
|
||||||
|
prefs.edit { clear() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package com.bitchat.android.storage
|
||||||
|
|
||||||
|
enum class StorageSecurity {
|
||||||
|
NORMAL,
|
||||||
|
SECURE
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PanicClearPolicy {
|
||||||
|
CLEAR_ON_PANIC,
|
||||||
|
KEEP_ON_PANIC
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class StorageClearMode {
|
||||||
|
CLEAR_SCOPE,
|
||||||
|
REMOVE_OWNED_KEYS
|
||||||
|
}
|
||||||
|
|
||||||
|
data class StorageDefinition(
|
||||||
|
val id: String,
|
||||||
|
val owner: String,
|
||||||
|
val prefsName: String,
|
||||||
|
val security: StorageSecurity,
|
||||||
|
val panicClearPolicy: PanicClearPolicy,
|
||||||
|
val clearMode: StorageClearMode = StorageClearMode.CLEAR_SCOPE,
|
||||||
|
val ownedKeys: Set<String> = emptySet()
|
||||||
|
)
|
||||||
|
|
||||||
|
object StorageDefinitions {
|
||||||
|
val Chat = StorageDefinition(
|
||||||
|
id = "chat",
|
||||||
|
owner = "DataManager",
|
||||||
|
prefsName = "bitchat_prefs",
|
||||||
|
security = StorageSecurity.NORMAL,
|
||||||
|
panicClearPolicy = PanicClearPolicy.CLEAR_ON_PANIC
|
||||||
|
)
|
||||||
|
|
||||||
|
val GeohashBookmarks = StorageDefinition(
|
||||||
|
id = "geohash_bookmarks",
|
||||||
|
owner = "GeohashBookmarksStore",
|
||||||
|
prefsName = "geohash_prefs",
|
||||||
|
security = StorageSecurity.NORMAL,
|
||||||
|
panicClearPolicy = PanicClearPolicy.CLEAR_ON_PANIC
|
||||||
|
)
|
||||||
|
|
||||||
|
val GeohashAliasRegistry = StorageDefinition(
|
||||||
|
id = "geohash_alias_registry",
|
||||||
|
owner = "GeohashAliasRegistry",
|
||||||
|
prefsName = "geohash_alias_registry",
|
||||||
|
security = StorageSecurity.NORMAL,
|
||||||
|
panicClearPolicy = PanicClearPolicy.CLEAR_ON_PANIC
|
||||||
|
)
|
||||||
|
|
||||||
|
val GeohashConversationRegistry = StorageDefinition(
|
||||||
|
id = "geohash_conversation_registry",
|
||||||
|
owner = "GeohashConversationRegistry",
|
||||||
|
prefsName = "geohash_conversation_registry",
|
||||||
|
security = StorageSecurity.NORMAL,
|
||||||
|
panicClearPolicy = PanicClearPolicy.CLEAR_ON_PANIC
|
||||||
|
)
|
||||||
|
|
||||||
|
val Favorites = StorageDefinition(
|
||||||
|
id = "favorites",
|
||||||
|
owner = "FavoritesPersistenceService",
|
||||||
|
prefsName = "bitchat_identity",
|
||||||
|
security = StorageSecurity.SECURE,
|
||||||
|
panicClearPolicy = PanicClearPolicy.CLEAR_ON_PANIC,
|
||||||
|
clearMode = StorageClearMode.REMOVE_OWNED_KEYS,
|
||||||
|
ownedKeys = setOf("favorite_relationships", "favorite_peerid_index")
|
||||||
|
)
|
||||||
|
|
||||||
|
val SeenMessages = StorageDefinition(
|
||||||
|
id = "seen_messages",
|
||||||
|
owner = "SeenMessageStore",
|
||||||
|
prefsName = "bitchat_identity",
|
||||||
|
security = StorageSecurity.SECURE,
|
||||||
|
panicClearPolicy = PanicClearPolicy.CLEAR_ON_PANIC,
|
||||||
|
clearMode = StorageClearMode.REMOVE_OWNED_KEYS,
|
||||||
|
ownedKeys = setOf("seen_message_store_v1")
|
||||||
|
)
|
||||||
|
|
||||||
|
val MeshServicePreferences = StorageDefinition(
|
||||||
|
id = "mesh_service_preferences",
|
||||||
|
owner = "MeshServicePreferences",
|
||||||
|
prefsName = "bitchat_mesh_service_prefs",
|
||||||
|
security = StorageSecurity.NORMAL,
|
||||||
|
panicClearPolicy = PanicClearPolicy.KEEP_ON_PANIC
|
||||||
|
)
|
||||||
|
|
||||||
|
val DebugPreferences = StorageDefinition(
|
||||||
|
id = "debug_preferences",
|
||||||
|
owner = "DebugPreferenceManager",
|
||||||
|
prefsName = "bitchat_debug_settings",
|
||||||
|
security = StorageSecurity.NORMAL,
|
||||||
|
panicClearPolicy = PanicClearPolicy.KEEP_ON_PANIC
|
||||||
|
)
|
||||||
|
|
||||||
|
val PowPreferences = StorageDefinition(
|
||||||
|
id = "pow_preferences",
|
||||||
|
owner = "PoWPreferenceManager",
|
||||||
|
prefsName = "pow_preferences",
|
||||||
|
security = StorageSecurity.NORMAL,
|
||||||
|
panicClearPolicy = PanicClearPolicy.KEEP_ON_PANIC
|
||||||
|
)
|
||||||
|
|
||||||
|
val AppSettings = StorageDefinition(
|
||||||
|
id = "app_settings",
|
||||||
|
owner = "TorPreferenceManager, ThemePreferenceManager, and onboarding preference managers",
|
||||||
|
prefsName = "bitchat_settings",
|
||||||
|
security = StorageSecurity.NORMAL,
|
||||||
|
panicClearPolicy = PanicClearPolicy.KEEP_ON_PANIC
|
||||||
|
)
|
||||||
|
|
||||||
|
val OnboardingPermissions = StorageDefinition(
|
||||||
|
id = "onboarding_permissions",
|
||||||
|
owner = "PermissionManager",
|
||||||
|
prefsName = "bitchat_permissions",
|
||||||
|
security = StorageSecurity.NORMAL,
|
||||||
|
panicClearPolicy = PanicClearPolicy.KEEP_ON_PANIC
|
||||||
|
)
|
||||||
|
|
||||||
|
val panicClearDefinitions = listOf(
|
||||||
|
Chat,
|
||||||
|
GeohashBookmarks,
|
||||||
|
GeohashAliasRegistry,
|
||||||
|
GeohashConversationRegistry,
|
||||||
|
Favorites,
|
||||||
|
SeenMessages
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.bitchat.android.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.gson.Gson
|
||||||
|
|
||||||
|
object StorageModule {
|
||||||
|
private val gson = Gson()
|
||||||
|
|
||||||
|
fun repository(
|
||||||
|
context: Context,
|
||||||
|
definition: StorageDefinition,
|
||||||
|
migrations: List<StorageMigration> = emptyList()
|
||||||
|
): StorageRepository {
|
||||||
|
val appContext = context.applicationContext
|
||||||
|
val store = when (definition.security) {
|
||||||
|
StorageSecurity.NORMAL -> SharedPreferencesKeyValueStore(
|
||||||
|
appContext.getSharedPreferences(definition.prefsName, Context.MODE_PRIVATE)
|
||||||
|
)
|
||||||
|
StorageSecurity.SECURE -> SecureIdentityKeyValueStore(appContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
val repository = StorageRepository(definition, store, gson)
|
||||||
|
repository.runMigrations(migrations)
|
||||||
|
PanicClearRegistry.registerIfAbsent(definition) { repository.clearForPanic() }
|
||||||
|
return repository
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerKnownPanicStores(context: Context) {
|
||||||
|
StorageDefinitions.panicClearDefinitions.forEach { definition ->
|
||||||
|
repository(context, definition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.bitchat.android.storage
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
|
data class StorageMigration(
|
||||||
|
val toVersion: Int,
|
||||||
|
val migrate: (StorageRepository) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
class StorageRepository(
|
||||||
|
val definition: StorageDefinition,
|
||||||
|
private val store: KeyValueStore,
|
||||||
|
private val gson: Gson = Gson()
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun getString(key: String, defaultValue: String? = null): String? = store.getString(key, defaultValue)
|
||||||
|
fun putString(key: String, value: String?) = store.putString(key, value)
|
||||||
|
fun getBoolean(key: String, defaultValue: Boolean): Boolean = store.getBoolean(key, defaultValue)
|
||||||
|
fun putBoolean(key: String, value: Boolean) = store.putBoolean(key, value)
|
||||||
|
fun getInt(key: String, defaultValue: Int): Int = store.getInt(key, defaultValue)
|
||||||
|
fun putInt(key: String, value: Int) = store.putInt(key, value)
|
||||||
|
fun getLong(key: String, defaultValue: Long): Long = store.getLong(key, defaultValue)
|
||||||
|
fun putLong(key: String, value: Long) = store.putLong(key, value)
|
||||||
|
fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Set<String> {
|
||||||
|
return store.getStringSet(key, defaultValue)
|
||||||
|
}
|
||||||
|
fun putStringSet(key: String, value: Set<String>) = store.putStringSet(key, value)
|
||||||
|
fun remove(key: String) = store.remove(key)
|
||||||
|
fun contains(key: String): Boolean = store.contains(key)
|
||||||
|
|
||||||
|
fun <T> getJson(key: String, type: Type): T? {
|
||||||
|
val json = store.getString(key, null) ?: return null
|
||||||
|
return runCatching { gson.fromJson<T>(json, type) }.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> getJson(key: String, clazz: Class<T>): T? {
|
||||||
|
val json = store.getString(key, null) ?: return null
|
||||||
|
return runCatching { gson.fromJson(json, clazz) }.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putJson(key: String, value: Any?) {
|
||||||
|
if (value == null) {
|
||||||
|
store.remove(key)
|
||||||
|
} else {
|
||||||
|
store.putString(key, gson.toJson(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runMigrations(migrations: List<StorageMigration>) {
|
||||||
|
var currentVersion = getInt(VERSION_KEY, 0)
|
||||||
|
migrations
|
||||||
|
.sortedBy { it.toVersion }
|
||||||
|
.filter { it.toVersion > currentVersion }
|
||||||
|
.forEach { migration ->
|
||||||
|
migration.migrate(this)
|
||||||
|
currentVersion = migration.toVersion
|
||||||
|
putInt(VERSION_KEY, currentVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearForPanic() {
|
||||||
|
if (definition.panicClearPolicy != PanicClearPolicy.CLEAR_ON_PANIC) return
|
||||||
|
|
||||||
|
when (definition.clearMode) {
|
||||||
|
StorageClearMode.CLEAR_SCOPE -> store.clear()
|
||||||
|
StorageClearMode.REMOVE_OWNED_KEYS -> definition.ownedKeys.forEach { store.remove(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val VERSION_KEY = "__storage_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import android.util.Log
|
|||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.bitchat.android.favorites.FavoritesPersistenceService
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -21,13 +20,13 @@ import com.bitchat.android.protocol.BitchatPacket
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.bitchat.android.util.NotificationIntervalManager
|
import com.bitchat.android.util.NotificationIntervalManager
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import com.bitchat.android.services.VerificationService
|
import com.bitchat.android.services.VerificationService
|
||||||
import com.bitchat.android.identity.SecureIdentityStateManager
|
import com.bitchat.android.identity.SecureIdentityStateManager
|
||||||
import com.bitchat.android.noise.NoiseSession
|
import com.bitchat.android.noise.NoiseSession
|
||||||
import com.bitchat.android.nostr.GeohashAliasRegistry
|
import com.bitchat.android.storage.PanicClearRegistry
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
import com.bitchat.android.util.dataFromHexString
|
import com.bitchat.android.util.dataFromHexString
|
||||||
import com.bitchat.android.util.hexEncodedString
|
import com.bitchat.android.util.hexEncodedString
|
||||||
|
|
||||||
@@ -910,12 +909,18 @@ class ChatViewModel(
|
|||||||
messageManager.clearAllMessages()
|
messageManager.clearAllMessages()
|
||||||
channelManager.clearAllChannels()
|
channelManager.clearAllChannels()
|
||||||
privateChatManager.clearAllPrivateChats()
|
privateChatManager.clearAllPrivateChats()
|
||||||
dataManager.clearAllData()
|
|
||||||
|
|
||||||
// Clear seen message store
|
|
||||||
try {
|
try {
|
||||||
com.bitchat.android.services.SeenMessageStore.getInstance(getApplication()).clear()
|
StorageModule.registerKnownPanicStores(getApplication())
|
||||||
} catch (_: Exception) { }
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to register all panic stores: ${e.message}")
|
||||||
|
}
|
||||||
|
PanicClearRegistry.clearAll().forEach { result ->
|
||||||
|
if (result.success) {
|
||||||
|
Log.d(TAG, "✅ Panic-cleared ${result.owner} (${result.id})")
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "❌ Panic clear failed for ${result.owner} (${result.id}): ${result.errorMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear all mesh service data
|
// Clear all mesh service data
|
||||||
clearAllMeshServiceData()
|
clearAllMeshServiceData()
|
||||||
@@ -929,14 +934,8 @@ class ChatViewModel(
|
|||||||
// Clear all media files
|
// Clear all media files
|
||||||
com.bitchat.android.features.file.FileUtils.clearAllMedia(getApplication())
|
com.bitchat.android.features.file.FileUtils.clearAllMedia(getApplication())
|
||||||
|
|
||||||
// Clear Nostr/geohash state, keys, connections, bookmarks, and reinitialize from scratch
|
// Reset Nostr/geohash runtime state and reinitialize from scratch
|
||||||
try {
|
try {
|
||||||
// Clear geohash bookmarks too (panic should remove everything)
|
|
||||||
try {
|
|
||||||
val store = com.bitchat.android.geohash.GeohashBookmarksStore.getInstance(getApplication())
|
|
||||||
store.clearAll()
|
|
||||||
} catch (_: Exception) { }
|
|
||||||
|
|
||||||
geohashViewModel.panicReset()
|
geohashViewModel.panicReset()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to reset Nostr/geohash: ${e.message}")
|
Log.e(TAG, "Failed to reset Nostr/geohash: ${e.message}")
|
||||||
@@ -1006,20 +1005,10 @@ class ChatViewModel(
|
|||||||
try {
|
try {
|
||||||
val identityManager = SecureIdentityStateManager(getApplication())
|
val identityManager = SecureIdentityStateManager(getApplication())
|
||||||
identityManager.clearIdentityData()
|
identityManager.clearIdentityData()
|
||||||
// Also clear secure values used by FavoritesPersistenceService (favorites + peerID index)
|
Log.d(TAG, "✅ Cleared secure identity state")
|
||||||
try {
|
|
||||||
identityManager.clearSecureValues("favorite_relationships", "favorite_peerid_index")
|
|
||||||
} catch (_: Exception) { }
|
|
||||||
Log.d(TAG, "✅ Cleared secure identity state and secure favorites store")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(TAG, "SecureIdentityStateManager not available or already cleared: ${e.message}")
|
Log.d(TAG, "SecureIdentityStateManager not available or already cleared: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear FavoritesPersistenceService persistent relationships
|
|
||||||
try {
|
|
||||||
FavoritesPersistenceService.shared.clearAllFavorites()
|
|
||||||
Log.d(TAG, "✅ Cleared FavoritesPersistenceService relationships")
|
|
||||||
} catch (_: Exception) { }
|
|
||||||
|
|
||||||
Log.d(TAG, "✅ Cleared all cryptographic data")
|
Log.d(TAG, "✅ Cleared all cryptographic data")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.bitchat.android.ui
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.bitchat.android.storage.PanicClearRegistry
|
||||||
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ class DataManager(private val context: Context) {
|
|||||||
private const val TAG = "DataManager"
|
private const val TAG = "DataManager"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val prefs: SharedPreferences = context.getSharedPreferences("bitchat_prefs", Context.MODE_PRIVATE)
|
private val storage = StorageModule.repository(context, StorageDefinitions.Chat)
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
|
|
||||||
// Channel-related maps that need to persist state
|
// Channel-related maps that need to persist state
|
||||||
@@ -28,11 +30,15 @@ class DataManager(private val context: Context) {
|
|||||||
val favoritePeers: Set<String> get() = _favoritePeers
|
val favoritePeers: Set<String> get() = _favoritePeers
|
||||||
val blockedUsers: Set<String> get() = _blockedUsers
|
val blockedUsers: Set<String> get() = _blockedUsers
|
||||||
val channelMembers: Map<String, MutableSet<String>> get() = _channelMembers
|
val channelMembers: Map<String, MutableSet<String>> get() = _channelMembers
|
||||||
|
|
||||||
|
init {
|
||||||
|
PanicClearRegistry.register(StorageDefinitions.Chat) { clearAllData() }
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Nickname Management
|
// MARK: - Nickname Management
|
||||||
|
|
||||||
fun loadNickname(): String {
|
fun loadNickname(): String {
|
||||||
val savedNickname = prefs.getString("nickname", null)
|
val savedNickname = storage.getString("nickname", null)
|
||||||
return if (savedNickname != null) {
|
return if (savedNickname != null) {
|
||||||
savedNickname
|
savedNickname
|
||||||
} else {
|
} else {
|
||||||
@@ -43,47 +49,47 @@ class DataManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveNickname(nickname: String) {
|
fun saveNickname(nickname: String) {
|
||||||
prefs.edit().putString("nickname", nickname).apply()
|
storage.putString("nickname", nickname)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Geohash Channel Persistence
|
// MARK: - Geohash Channel Persistence
|
||||||
|
|
||||||
fun loadLastGeohashChannel(): String? {
|
fun loadLastGeohashChannel(): String? {
|
||||||
return prefs.getString("last_geohash_channel", null)
|
return storage.getString("last_geohash_channel", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveLastGeohashChannel(channelData: String) {
|
fun saveLastGeohashChannel(channelData: String) {
|
||||||
prefs.edit().putString("last_geohash_channel", channelData).apply()
|
storage.putString("last_geohash_channel", channelData)
|
||||||
Log.d(TAG, "Saved last geohash channel: $channelData")
|
Log.d(TAG, "Saved last geohash channel: $channelData")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearLastGeohashChannel() {
|
fun clearLastGeohashChannel() {
|
||||||
prefs.edit().remove("last_geohash_channel").apply()
|
storage.remove("last_geohash_channel")
|
||||||
Log.d(TAG, "Cleared last geohash channel")
|
Log.d(TAG, "Cleared last geohash channel")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Location Services State
|
// MARK: - Location Services State
|
||||||
|
|
||||||
fun saveLocationServicesEnabled(enabled: Boolean) {
|
fun saveLocationServicesEnabled(enabled: Boolean) {
|
||||||
prefs.edit().putBoolean("location_services_enabled", enabled).apply()
|
storage.putBoolean("location_services_enabled", enabled)
|
||||||
Log.d(TAG, "Saved location services enabled state: $enabled")
|
Log.d(TAG, "Saved location services enabled state: $enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isLocationServicesEnabled(): Boolean {
|
fun isLocationServicesEnabled(): Boolean {
|
||||||
return prefs.getBoolean("location_services_enabled", true) // Default to enabled
|
return storage.getBoolean("location_services_enabled", true) // Default to enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Channel Data Management
|
// MARK: - Channel Data Management
|
||||||
|
|
||||||
fun loadChannelData(): Pair<Set<String>, Set<String>> {
|
fun loadChannelData(): Pair<Set<String>, Set<String>> {
|
||||||
// Load joined channels
|
// Load joined channels
|
||||||
val savedChannels = prefs.getStringSet("joined_channels", emptySet()) ?: emptySet()
|
val savedChannels = storage.getStringSet("joined_channels", emptySet())
|
||||||
|
|
||||||
// Load password protected channels
|
// Load password protected channels
|
||||||
val savedProtectedChannels = prefs.getStringSet("password_protected_channels", emptySet()) ?: emptySet()
|
val savedProtectedChannels = storage.getStringSet("password_protected_channels", emptySet())
|
||||||
|
|
||||||
// Load channel creators
|
// Load channel creators
|
||||||
val creatorsJson = prefs.getString("channel_creators", "{}")
|
val creatorsJson = storage.getString("channel_creators", "{}")
|
||||||
try {
|
try {
|
||||||
val creatorsMap = gson.fromJson(creatorsJson, Map::class.java) as? Map<String, String>
|
val creatorsMap = gson.fromJson(creatorsJson, Map::class.java) as? Map<String, String>
|
||||||
creatorsMap?.let { _channelCreators.putAll(it) }
|
creatorsMap?.let { _channelCreators.putAll(it) }
|
||||||
@@ -102,12 +108,9 @@ class DataManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveChannelData(joinedChannels: Set<String>, passwordProtectedChannels: Set<String>) {
|
fun saveChannelData(joinedChannels: Set<String>, passwordProtectedChannels: Set<String>) {
|
||||||
prefs.edit().apply {
|
storage.putStringSet("joined_channels", joinedChannels)
|
||||||
putStringSet("joined_channels", joinedChannels)
|
storage.putStringSet("password_protected_channels", passwordProtectedChannels)
|
||||||
putStringSet("password_protected_channels", passwordProtectedChannels)
|
storage.putString("channel_creators", gson.toJson(_channelCreators))
|
||||||
putString("channel_creators", gson.toJson(_channelCreators))
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addChannelCreator(channel: String, creatorID: String) {
|
fun addChannelCreator(channel: String, creatorID: String) {
|
||||||
@@ -156,13 +159,13 @@ class DataManager(private val context: Context) {
|
|||||||
// MARK: - Favorites Management
|
// MARK: - Favorites Management
|
||||||
|
|
||||||
fun loadFavorites() {
|
fun loadFavorites() {
|
||||||
val savedFavorites = prefs.getStringSet("favorites", emptySet()) ?: emptySet()
|
val savedFavorites = storage.getStringSet("favorites", emptySet())
|
||||||
_favoritePeers.addAll(savedFavorites)
|
_favoritePeers.addAll(savedFavorites)
|
||||||
Log.d(TAG, "Loaded ${savedFavorites.size} favorite users from storage: $savedFavorites")
|
Log.d(TAG, "Loaded ${savedFavorites.size} favorite users from storage: $savedFavorites")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveFavorites() {
|
fun saveFavorites() {
|
||||||
prefs.edit().putStringSet("favorites", _favoritePeers).apply()
|
storage.putStringSet("favorites", _favoritePeers)
|
||||||
Log.d(TAG, "Saved ${_favoritePeers.size} favorite users to storage: $_favoritePeers")
|
Log.d(TAG, "Saved ${_favoritePeers.size} favorite users to storage: $_favoritePeers")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,12 +201,12 @@ class DataManager(private val context: Context) {
|
|||||||
// MARK: - Blocked Users Management
|
// MARK: - Blocked Users Management
|
||||||
|
|
||||||
fun loadBlockedUsers() {
|
fun loadBlockedUsers() {
|
||||||
val savedBlockedUsers = prefs.getStringSet("blocked_users", emptySet()) ?: emptySet()
|
val savedBlockedUsers = storage.getStringSet("blocked_users", emptySet())
|
||||||
_blockedUsers.addAll(savedBlockedUsers)
|
_blockedUsers.addAll(savedBlockedUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveBlockedUsers() {
|
fun saveBlockedUsers() {
|
||||||
prefs.edit().putStringSet("blocked_users", _blockedUsers).apply()
|
storage.putStringSet("blocked_users", _blockedUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addBlockedUser(fingerprint: String) {
|
fun addBlockedUser(fingerprint: String) {
|
||||||
@@ -226,12 +229,12 @@ class DataManager(private val context: Context) {
|
|||||||
val geohashBlockedUsers: Set<String> get() = _geohashBlockedUsers.toSet()
|
val geohashBlockedUsers: Set<String> get() = _geohashBlockedUsers.toSet()
|
||||||
|
|
||||||
fun loadGeohashBlockedUsers() {
|
fun loadGeohashBlockedUsers() {
|
||||||
val savedGeohashBlockedUsers = prefs.getStringSet("geohash_blocked_users", emptySet()) ?: emptySet()
|
val savedGeohashBlockedUsers = storage.getStringSet("geohash_blocked_users", emptySet())
|
||||||
_geohashBlockedUsers.addAll(savedGeohashBlockedUsers)
|
_geohashBlockedUsers.addAll(savedGeohashBlockedUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveGeohashBlockedUsers() {
|
fun saveGeohashBlockedUsers() {
|
||||||
prefs.edit().putStringSet("geohash_blocked_users", _geohashBlockedUsers).apply()
|
storage.putStringSet("geohash_blocked_users", _geohashBlockedUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addGeohashBlockedUser(pubkeyHex: String) {
|
fun addGeohashBlockedUser(pubkeyHex: String) {
|
||||||
@@ -256,6 +259,6 @@ class DataManager(private val context: Context) {
|
|||||||
_blockedUsers.clear()
|
_blockedUsers.clear()
|
||||||
_geohashBlockedUsers.clear()
|
_geohashBlockedUsers.clear()
|
||||||
_channelMembers.clear()
|
_channelMembers.clear()
|
||||||
prefs.edit().clear().apply()
|
storage.clearForPanic()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,8 +160,6 @@ class GeohashViewModel(
|
|||||||
|
|
||||||
fun panicReset() {
|
fun panicReset() {
|
||||||
repo.clearAll()
|
repo.clearAll()
|
||||||
GeohashAliasRegistry.clear()
|
|
||||||
GeohashConversationRegistry.clear()
|
|
||||||
subscriptionManager.disconnect()
|
subscriptionManager.disconnect()
|
||||||
currentGeohashSubId = null
|
currentGeohashSubId = null
|
||||||
currentDmSubId = null
|
currentDmSubId = null
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
package com.bitchat.android.ui.debug
|
package com.bitchat.android.ui.debug
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
|
import com.bitchat.android.storage.StorageRepository
|
||||||
|
|
||||||
/**
|
|
||||||
* SharedPreferences-backed persistence for debug settings.
|
|
||||||
* Keeps the DebugSettingsManager stateless with regard to Android Context.
|
|
||||||
*/
|
|
||||||
object DebugPreferenceManager {
|
object DebugPreferenceManager {
|
||||||
private const val PREFS_NAME = "bitchat_debug_settings"
|
|
||||||
private const val KEY_VERBOSE = "verbose_logging"
|
private const val KEY_VERBOSE = "verbose_logging"
|
||||||
private const val KEY_GATT_SERVER = "gatt_server_enabled"
|
private const val KEY_GATT_SERVER = "gatt_server_enabled"
|
||||||
private const val KEY_GATT_CLIENT = "gatt_client_enabled"
|
private const val KEY_GATT_CLIENT = "gatt_client_enabled"
|
||||||
@@ -20,87 +17,82 @@ object DebugPreferenceManager {
|
|||||||
// GCS keys (no migration/back-compat)
|
// GCS keys (no migration/back-compat)
|
||||||
private const val KEY_GCS_MAX_BYTES = "gcs_max_filter_bytes"
|
private const val KEY_GCS_MAX_BYTES = "gcs_max_filter_bytes"
|
||||||
private const val KEY_GCS_FPR = "gcs_filter_fpr_percent"
|
private const val KEY_GCS_FPR = "gcs_filter_fpr_percent"
|
||||||
// Removed: persistent notification toggle is now governed by MeshServicePreferences.isBackgroundEnabled
|
|
||||||
|
|
||||||
private lateinit var prefs: SharedPreferences
|
private lateinit var storage: StorageRepository
|
||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
storage = StorageModule.repository(context, StorageDefinitions.DebugPreferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ready(): Boolean = ::prefs.isInitialized
|
private fun ready(): Boolean = ::storage.isInitialized
|
||||||
|
|
||||||
fun getVerboseLogging(default: Boolean = false): Boolean =
|
fun getVerboseLogging(default: Boolean = false): Boolean =
|
||||||
if (ready()) prefs.getBoolean(KEY_VERBOSE, default) else default
|
if (ready()) storage.getBoolean(KEY_VERBOSE, default) else default
|
||||||
|
|
||||||
fun setVerboseLogging(value: Boolean) {
|
fun setVerboseLogging(value: Boolean) {
|
||||||
if (ready()) prefs.edit().putBoolean(KEY_VERBOSE, value).apply()
|
if (ready()) storage.putBoolean(KEY_VERBOSE, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGattServerEnabled(default: Boolean = true): Boolean =
|
fun getGattServerEnabled(default: Boolean = true): Boolean =
|
||||||
if (ready()) prefs.getBoolean(KEY_GATT_SERVER, default) else default
|
if (ready()) storage.getBoolean(KEY_GATT_SERVER, default) else default
|
||||||
|
|
||||||
fun setGattServerEnabled(value: Boolean) {
|
fun setGattServerEnabled(value: Boolean) {
|
||||||
if (ready()) prefs.edit().putBoolean(KEY_GATT_SERVER, value).apply()
|
if (ready()) storage.putBoolean(KEY_GATT_SERVER, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGattClientEnabled(default: Boolean = true): Boolean =
|
fun getGattClientEnabled(default: Boolean = true): Boolean =
|
||||||
if (ready()) prefs.getBoolean(KEY_GATT_CLIENT, default) else default
|
if (ready()) storage.getBoolean(KEY_GATT_CLIENT, default) else default
|
||||||
|
|
||||||
fun setGattClientEnabled(value: Boolean) {
|
fun setGattClientEnabled(value: Boolean) {
|
||||||
if (ready()) prefs.edit().putBoolean(KEY_GATT_CLIENT, value).apply()
|
if (ready()) storage.putBoolean(KEY_GATT_CLIENT, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPacketRelayEnabled(default: Boolean = true): Boolean =
|
fun getPacketRelayEnabled(default: Boolean = true): Boolean =
|
||||||
if (ready()) prefs.getBoolean(KEY_PACKET_RELAY, default) else default
|
if (ready()) storage.getBoolean(KEY_PACKET_RELAY, default) else default
|
||||||
|
|
||||||
fun setPacketRelayEnabled(value: Boolean) {
|
fun setPacketRelayEnabled(value: Boolean) {
|
||||||
if (ready()) prefs.edit().putBoolean(KEY_PACKET_RELAY, value).apply()
|
if (ready()) storage.putBoolean(KEY_PACKET_RELAY, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional connection limits (0 or missing => use defaults)
|
|
||||||
fun getMaxConnectionsOverall(default: Int = 8): Int =
|
fun getMaxConnectionsOverall(default: Int = 8): Int =
|
||||||
if (ready()) prefs.getInt(KEY_MAX_CONN_OVERALL, default) else default
|
if (ready()) storage.getInt(KEY_MAX_CONN_OVERALL, default) else default
|
||||||
|
|
||||||
fun setMaxConnectionsOverall(value: Int) {
|
fun setMaxConnectionsOverall(value: Int) {
|
||||||
if (ready()) prefs.edit().putInt(KEY_MAX_CONN_OVERALL, value).apply()
|
if (ready()) storage.putInt(KEY_MAX_CONN_OVERALL, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMaxConnectionsServer(default: Int = 8): Int =
|
fun getMaxConnectionsServer(default: Int = 8): Int =
|
||||||
if (ready()) prefs.getInt(KEY_MAX_CONN_SERVER, default) else default
|
if (ready()) storage.getInt(KEY_MAX_CONN_SERVER, default) else default
|
||||||
|
|
||||||
fun setMaxConnectionsServer(value: Int) {
|
fun setMaxConnectionsServer(value: Int) {
|
||||||
if (ready()) prefs.edit().putInt(KEY_MAX_CONN_SERVER, value).apply()
|
if (ready()) storage.putInt(KEY_MAX_CONN_SERVER, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMaxConnectionsClient(default: Int = 8): Int =
|
fun getMaxConnectionsClient(default: Int = 8): Int =
|
||||||
if (ready()) prefs.getInt(KEY_MAX_CONN_CLIENT, default) else default
|
if (ready()) storage.getInt(KEY_MAX_CONN_CLIENT, default) else default
|
||||||
|
|
||||||
fun setMaxConnectionsClient(value: Int) {
|
fun setMaxConnectionsClient(value: Int) {
|
||||||
if (ready()) prefs.edit().putInt(KEY_MAX_CONN_CLIENT, value).apply()
|
if (ready()) storage.putInt(KEY_MAX_CONN_CLIENT, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync/GCS settings
|
|
||||||
fun getSeenPacketCapacity(default: Int = 500): Int =
|
fun getSeenPacketCapacity(default: Int = 500): Int =
|
||||||
if (ready()) prefs.getInt(KEY_SEEN_PACKET_CAP, default) else default
|
if (ready()) storage.getInt(KEY_SEEN_PACKET_CAP, default) else default
|
||||||
|
|
||||||
fun setSeenPacketCapacity(value: Int) {
|
fun setSeenPacketCapacity(value: Int) {
|
||||||
if (ready()) prefs.edit().putInt(KEY_SEEN_PACKET_CAP, value).apply()
|
if (ready()) storage.putInt(KEY_SEEN_PACKET_CAP, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGcsMaxFilterBytes(default: Int = 400): Int =
|
fun getGcsMaxFilterBytes(default: Int = 400): Int =
|
||||||
if (ready()) prefs.getInt(KEY_GCS_MAX_BYTES, default) else default
|
if (ready()) storage.getInt(KEY_GCS_MAX_BYTES, default) else default
|
||||||
|
|
||||||
fun setGcsMaxFilterBytes(value: Int) {
|
fun setGcsMaxFilterBytes(value: Int) {
|
||||||
if (ready()) prefs.edit().putInt(KEY_GCS_MAX_BYTES, value).apply()
|
if (ready()) storage.putInt(KEY_GCS_MAX_BYTES, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGcsFprPercent(default: Double = 1.0): Double =
|
fun getGcsFprPercent(default: Double = 1.0): Double =
|
||||||
if (ready()) java.lang.Double.longBitsToDouble(prefs.getLong(KEY_GCS_FPR, java.lang.Double.doubleToRawLongBits(default))) else default
|
if (ready()) java.lang.Double.longBitsToDouble(storage.getLong(KEY_GCS_FPR, java.lang.Double.doubleToRawLongBits(default))) else default
|
||||||
|
|
||||||
fun setGcsFprPercent(value: Double) {
|
fun setGcsFprPercent(value: Double) {
|
||||||
if (ready()) prefs.edit().putLong(KEY_GCS_FPR, java.lang.Double.doubleToRawLongBits(value)).apply()
|
if (ready()) storage.putLong(KEY_GCS_FPR, java.lang.Double.doubleToRawLongBits(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
// No longer storing persistent notification in debug prefs.
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.bitchat.android.ui.theme
|
package com.bitchat.android.ui.theme
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.bitchat.android.storage.StorageDefinitions
|
||||||
|
import com.bitchat.android.storage.StorageModule
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
@@ -17,26 +19,21 @@ enum class ThemePreference {
|
|||||||
val isDark : Boolean get() = this == Dark
|
val isDark : Boolean get() = this == Dark
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple SharedPreferences-backed manager for theme preference with a StateFlow.
|
|
||||||
* Avoids adding DataStore dependency for now.
|
|
||||||
*/
|
|
||||||
object ThemePreferenceManager {
|
object ThemePreferenceManager {
|
||||||
private const val PREFS_NAME = "bitchat_settings"
|
|
||||||
private const val KEY_THEME = "theme_preference"
|
private const val KEY_THEME = "theme_preference"
|
||||||
|
|
||||||
private val _themeFlow = MutableStateFlow(ThemePreference.System)
|
private val _themeFlow = MutableStateFlow(ThemePreference.System)
|
||||||
val themeFlow: StateFlow<ThemePreference> = _themeFlow
|
val themeFlow: StateFlow<ThemePreference> = _themeFlow
|
||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
val storage = StorageModule.repository(context, StorageDefinitions.AppSettings)
|
||||||
val saved = prefs.getString(KEY_THEME, ThemePreference.System.name)
|
val saved = storage.getString(KEY_THEME, ThemePreference.System.name)
|
||||||
_themeFlow.value = runCatching { ThemePreference.valueOf(saved!!) }.getOrDefault(ThemePreference.System)
|
_themeFlow.value = runCatching { ThemePreference.valueOf(saved!!) }.getOrDefault(ThemePreference.System)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun set(context: Context, preference: ThemePreference) {
|
fun set(context: Context, preference: ThemePreference) {
|
||||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
val storage = StorageModule.repository(context, StorageDefinitions.AppSettings)
|
||||||
prefs.edit().putString(KEY_THEME, preference.name).apply()
|
storage.putString(KEY_THEME, preference.name)
|
||||||
_themeFlow.value = preference
|
_themeFlow.value = preference
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package com.bitchat.android.storage
|
||||||
|
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class StorageRepositoryTest {
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
PanicClearRegistry.resetForTesting()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `runs migrations once in version order`() {
|
||||||
|
val store = FakeKeyValueStore()
|
||||||
|
val repository = StorageRepository(testDefinition(), store)
|
||||||
|
val applied = mutableListOf<Int>()
|
||||||
|
|
||||||
|
val migrations = listOf(
|
||||||
|
StorageMigration(toVersion = 2) {
|
||||||
|
applied += 2
|
||||||
|
it.putString("v2", "done")
|
||||||
|
},
|
||||||
|
StorageMigration(toVersion = 1) {
|
||||||
|
applied += 1
|
||||||
|
it.putString("v1", "done")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
repository.runMigrations(migrations)
|
||||||
|
repository.runMigrations(migrations)
|
||||||
|
|
||||||
|
assertEquals(listOf(1, 2), applied)
|
||||||
|
assertEquals("done", repository.getString("v1"))
|
||||||
|
assertEquals("done", repository.getString("v2"))
|
||||||
|
assertEquals(2, repository.getInt("__storage_version", 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `panic clear removes only owned keys when definition scopes keys`() {
|
||||||
|
val definition = testDefinition(
|
||||||
|
clearMode = StorageClearMode.REMOVE_OWNED_KEYS,
|
||||||
|
ownedKeys = setOf("owned_a", "owned_b")
|
||||||
|
)
|
||||||
|
val repository = StorageRepository(definition, FakeKeyValueStore())
|
||||||
|
|
||||||
|
repository.putString("owned_a", "a")
|
||||||
|
repository.putString("owned_b", "b")
|
||||||
|
repository.putString("unowned", "keep")
|
||||||
|
|
||||||
|
repository.clearForPanic()
|
||||||
|
|
||||||
|
assertNull(repository.getString("owned_a"))
|
||||||
|
assertNull(repository.getString("owned_b"))
|
||||||
|
assertEquals("keep", repository.getString("unowned"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `panic clear clears full scope for normal panic stores`() {
|
||||||
|
val repository = StorageRepository(testDefinition(), FakeKeyValueStore())
|
||||||
|
repository.putString("one", "1")
|
||||||
|
repository.putBoolean("two", true)
|
||||||
|
|
||||||
|
repository.clearForPanic()
|
||||||
|
|
||||||
|
assertFalse(repository.contains("one"))
|
||||||
|
assertFalse(repository.contains("two"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `panic clear does nothing for stores kept on panic`() {
|
||||||
|
val repository = StorageRepository(
|
||||||
|
testDefinition(panicClearPolicy = PanicClearPolicy.KEEP_ON_PANIC),
|
||||||
|
FakeKeyValueStore()
|
||||||
|
)
|
||||||
|
repository.putString("setting", "keep")
|
||||||
|
|
||||||
|
repository.clearForPanic()
|
||||||
|
|
||||||
|
assertEquals("keep", repository.getString("setting"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `registry reports successes and failures without stopping clear`() {
|
||||||
|
var successCalled = false
|
||||||
|
PanicClearRegistry.register(testDefinition(id = "success", owner = "SuccessStore")) {
|
||||||
|
successCalled = true
|
||||||
|
}
|
||||||
|
PanicClearRegistry.register(testDefinition(id = "failure", owner = "FailureStore")) {
|
||||||
|
error("boom")
|
||||||
|
}
|
||||||
|
|
||||||
|
val results = PanicClearRegistry.clearAll()
|
||||||
|
|
||||||
|
assertTrue(successCalled)
|
||||||
|
assertEquals(2, results.size)
|
||||||
|
assertTrue(results.first { it.id == "success" }.success)
|
||||||
|
val failure = results.first { it.id == "failure" }
|
||||||
|
assertFalse(failure.success)
|
||||||
|
assertEquals("boom", failure.errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `register if absent preserves richer manager clear handler`() {
|
||||||
|
val definition = testDefinition(id = "store")
|
||||||
|
val calls = mutableListOf<String>()
|
||||||
|
|
||||||
|
PanicClearRegistry.register(definition) { calls += "manager" }
|
||||||
|
PanicClearRegistry.registerIfAbsent(definition) { calls += "default" }
|
||||||
|
|
||||||
|
PanicClearRegistry.clearAll()
|
||||||
|
|
||||||
|
assertEquals(listOf("manager"), calls)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testDefinition(
|
||||||
|
id: String = "test_store",
|
||||||
|
owner: String = "TestStore",
|
||||||
|
panicClearPolicy: PanicClearPolicy = PanicClearPolicy.CLEAR_ON_PANIC,
|
||||||
|
clearMode: StorageClearMode = StorageClearMode.CLEAR_SCOPE,
|
||||||
|
ownedKeys: Set<String> = emptySet()
|
||||||
|
): StorageDefinition {
|
||||||
|
return StorageDefinition(
|
||||||
|
id = id,
|
||||||
|
owner = owner,
|
||||||
|
prefsName = "${id}_prefs",
|
||||||
|
security = StorageSecurity.NORMAL,
|
||||||
|
panicClearPolicy = panicClearPolicy,
|
||||||
|
clearMode = clearMode,
|
||||||
|
ownedKeys = ownedKeys
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeKeyValueStore : KeyValueStore {
|
||||||
|
private val values = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
|
override fun getString(key: String, defaultValue: String?): String? {
|
||||||
|
return values[key] as? String ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putString(key: String, value: String?) {
|
||||||
|
if (value == null) values.remove(key) else values[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBoolean(key: String, defaultValue: Boolean): Boolean {
|
||||||
|
return values[key] as? Boolean ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putBoolean(key: String, value: Boolean) {
|
||||||
|
values[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInt(key: String, defaultValue: Int): Int {
|
||||||
|
return values[key] as? Int ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putInt(key: String, value: Int) {
|
||||||
|
values[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLong(key: String, defaultValue: Long): Long {
|
||||||
|
return values[key] as? Long ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putLong(key: String, value: Long) {
|
||||||
|
values[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStringSet(key: String, defaultValue: Set<String>): Set<String> {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return (values[key] as? Set<String>)?.toSet() ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putStringSet(key: String, value: Set<String>) {
|
||||||
|
values[key] = value.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contains(key: String): Boolean = values.containsKey(key)
|
||||||
|
|
||||||
|
override fun remove(key: String) {
|
||||||
|
values.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear() {
|
||||||
|
values.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user