From 695c54f03c264e36b360010501e46261f02b9afc Mon Sep 17 00:00:00 2001 From: Amir Khodak Date: Fri, 22 May 2026 20:59:10 +0000 Subject: [PATCH] v1.8: fix black screen on resume and crash on sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/build.gradle.kts | 4 +- .../me/khodak/claudeusage/MainActivity.kt | 6 ++- .../claudeusage/data/PreferencesManager.kt | 53 +++++++++++++------ 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index feaa82e..5cd6263 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "me.khodak.claudeusage" minSdk = 26 targetSdk = 34 - versionCode = 8 - versionName = "1.7" + versionCode = 9 + versionName = "1.8" } signingConfigs { diff --git a/app/src/main/java/me/khodak/claudeusage/MainActivity.kt b/app/src/main/java/me/khodak/claudeusage/MainActivity.kt index d836264..b9dc91f 100644 --- a/app/src/main/java/me/khodak/claudeusage/MainActivity.kt +++ b/app/src/main/java/me/khodak/claudeusage/MainActivity.kt @@ -68,7 +68,10 @@ class MainActivity : AppCompatActivity() { val cached = prefs.getUsageData() updateUI(cached) 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 { repo.fetchUsage() } catch (e: Exception) { + if (e is kotlinx.coroutines.CancellationException) throw e prefs.getUsageData()?.copy(errorMessage = "Network error") ?: UsageData(errorMessage = "Network error") } diff --git a/app/src/main/java/me/khodak/claudeusage/data/PreferencesManager.kt b/app/src/main/java/me/khodak/claudeusage/data/PreferencesManager.kt index eb0996b..4029082 100644 --- a/app/src/main/java/me/khodak/claudeusage/data/PreferencesManager.kt +++ b/app/src/main/java/me/khodak/claudeusage/data/PreferencesManager.kt @@ -10,29 +10,22 @@ class PreferencesManager(context: Context) { private val gson = Gson() - private val securePrefs = 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 (e: Exception) { - context.getSharedPreferences("claude_secure_fallback", Context.MODE_PRIVATE) - } + private val securePrefs = createSecurePrefs(context) private val prefs = context.getSharedPreferences("claude_prefs", Context.MODE_PRIVATE) 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() { - securePrefs.edit().clear().apply() + try { securePrefs.edit().clear().apply() } catch (_: Exception) {} 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_ACTIVE_WEEK = "active_week" 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) + } + } + } } }