v1.8: fix black screen on resume and crash on sync
- onResume() no longer triggers a refresh every time — only fetches when data is >5 min stale, so returning to app shows cached data instantly without a loading spinner - Fix CancellationException being swallowed by catch(Exception) in refreshUsage(), which caused updates to run on a destroyed activity - EncryptedSharedPreferences key invalidation (caused by enabling/changing biometrics or screen lock) now deletes the stale encrypted file and recreates it cleanly, rather than silently using empty fallback prefs - Wrap all securePrefs read/write ops in try-catch so a mid-session Keystore failure degrades gracefully instead of crashing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "me.khodak.claudeusage"
|
applicationId = "me.khodak.claudeusage"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 8
|
versionCode = 9
|
||||||
versionName = "1.7"
|
versionName = "1.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -68,7 +68,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val cached = prefs.getUsageData()
|
val cached = prefs.getUsageData()
|
||||||
updateUI(cached)
|
updateUI(cached)
|
||||||
if (prefs.isLoggedIn()) {
|
if (prefs.isLoggedIn()) {
|
||||||
refreshUsage()
|
val staleMs = 5 * 60 * 1000L
|
||||||
|
if ((cached?.lastUpdated ?: 0L) < System.currentTimeMillis() - staleMs) {
|
||||||
|
refreshUsage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +82,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val data = try {
|
val data = try {
|
||||||
repo.fetchUsage()
|
repo.fetchUsage()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is kotlinx.coroutines.CancellationException) throw e
|
||||||
prefs.getUsageData()?.copy(errorMessage = "Network error")
|
prefs.getUsageData()?.copy(errorMessage = "Network error")
|
||||||
?: UsageData(errorMessage = "Network error")
|
?: UsageData(errorMessage = "Network error")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,29 +10,22 @@ class PreferencesManager(context: Context) {
|
|||||||
|
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
|
|
||||||
private val securePrefs = try {
|
private val securePrefs = createSecurePrefs(context)
|
||||||
val masterKey = MasterKey.Builder(context)
|
|
||||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
||||||
.build()
|
|
||||||
EncryptedSharedPreferences.create(
|
|
||||||
context, "claude_secure", masterKey,
|
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
context.getSharedPreferences("claude_secure_fallback", Context.MODE_PRIVATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val prefs = context.getSharedPreferences("claude_prefs", Context.MODE_PRIVATE)
|
private val prefs = context.getSharedPreferences("claude_prefs", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
fun saveCookies(cookies: String) {
|
fun saveCookies(cookies: String) {
|
||||||
securePrefs.edit().putString(KEY_COOKIES, cookies).apply()
|
try {
|
||||||
|
securePrefs.edit().putString(KEY_COOKIES, cookies).apply()
|
||||||
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCookies(): String? = securePrefs.getString(KEY_COOKIES, null)
|
fun getCookies(): String? = try {
|
||||||
|
securePrefs.getString(KEY_COOKIES, null)
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
|
||||||
fun clearSession() {
|
fun clearSession() {
|
||||||
securePrefs.edit().clear().apply()
|
try { securePrefs.edit().clear().apply() } catch (_: Exception) {}
|
||||||
prefs.edit().remove(KEY_ORG_ID).remove(KEY_SESSION_START).apply()
|
prefs.edit().remove(KEY_ORG_ID).remove(KEY_SESSION_START).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,5 +77,35 @@ class PreferencesManager(context: Context) {
|
|||||||
private const val KEY_USAGE_DATA = "usage_data"
|
private const val KEY_USAGE_DATA = "usage_data"
|
||||||
private const val KEY_ACTIVE_WEEK = "active_week"
|
private const val KEY_ACTIVE_WEEK = "active_week"
|
||||||
private const val KEY_ACTIVE_MASK = "active_mask"
|
private const val KEY_ACTIVE_MASK = "active_mask"
|
||||||
|
|
||||||
|
private fun createSecurePrefs(context: Context): android.content.SharedPreferences {
|
||||||
|
// First attempt: normal open
|
||||||
|
return try {
|
||||||
|
val masterKey = MasterKey.Builder(context)
|
||||||
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
|
.build()
|
||||||
|
EncryptedSharedPreferences.create(
|
||||||
|
context, "claude_secure", masterKey,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
|
)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Keystore key was invalidated (e.g., user added/changed biometrics or screen lock).
|
||||||
|
// Delete the stale encrypted file and recreate — user will need to re-login.
|
||||||
|
try {
|
||||||
|
context.deleteSharedPreferences("claude_secure")
|
||||||
|
val masterKey = MasterKey.Builder(context)
|
||||||
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
|
.build()
|
||||||
|
EncryptedSharedPreferences.create(
|
||||||
|
context, "claude_secure", masterKey,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
|
)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
context.getSharedPreferences("claude_secure_fallback", Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user