Skip to content

Commit f095314

Browse files
fix: according to PR review
1 parent aa018a7 commit f095314

3 files changed

Lines changed: 196 additions & 9 deletions

File tree

app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ import androidx.datastore.preferences.core.longPreferencesKey
99
import androidx.datastore.preferences.core.stringPreferencesKey
1010
import androidx.datastore.preferences.preferencesDataStore
1111
import com.google.gson.Gson
12+
import kotlinx.coroutines.CoroutineScope
13+
import kotlinx.coroutines.Dispatchers
14+
import kotlinx.coroutines.SupervisorJob
1215
import kotlinx.coroutines.flow.first
1316
import kotlinx.coroutines.flow.map
17+
import kotlinx.coroutines.launch
1418
import kotlinx.coroutines.runBlocking
1519
import org.openedx.core.data.model.User
1620
import org.openedx.core.data.storage.CalendarPreferences
@@ -21,15 +25,25 @@ import org.openedx.core.domain.model.CalendarType
2125
import org.openedx.core.domain.model.VideoQuality
2226
import org.openedx.core.domain.model.VideoSettings
2327
import org.openedx.core.system.CalendarManager
28+
import org.openedx.core.system.notifier.app.AppNotifier
29+
import org.openedx.core.system.notifier.app.LogoutEvent
2430
import org.openedx.course.data.storage.CoursePreferences
2531
import org.openedx.foundation.extension.replaceSpace
2632
import org.openedx.profile.data.model.Account
2733
import org.openedx.profile.data.storage.ProfilePreferences
2834
import org.openedx.whatsnew.data.storage.WhatsNewPreferences
2935

30-
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "openedx_prefs")
36+
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
37+
name = "openedx_prefs",
38+
produceMigrations = { context ->
39+
listOf(SharedPrefsToDataStoreMigration(context))
40+
}
41+
)
3142

32-
class PreferencesManager(private val context: Context) :
43+
class PreferencesManager(
44+
private val context: Context,
45+
private val appNotifier: AppNotifier,
46+
) :
3347
CorePreferences,
3448
ProfilePreferences,
3549
WhatsNewPreferences,
@@ -42,6 +56,11 @@ class PreferencesManager(private val context: Context) :
4256

4357
private val encryption = DataStoreEncryption()
4458

59+
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
60+
61+
@Volatile
62+
private var isKeyInvalidated = false
63+
4564
private object Keys {
4665
val ACCESS_TOKEN = stringPreferencesKey("access_token")
4766
val REFRESH_TOKEN = stringPreferencesKey("refresh_token")
@@ -71,19 +90,31 @@ class PreferencesManager(private val context: Context) :
7190
booleanPreferencesKey("calendar_sync_dialog_${courseName.replaceSpace("_")}")
7291
}
7392

74-
private fun <T> getValue(key: Preferences.Key<T>, defaultValue: T): T = runBlocking {
75-
dataStore.data.map { it[key] ?: defaultValue }.first()
76-
}
93+
private fun <T> getValue(key: Preferences.Key<T>, defaultValue: T): T =
94+
runBlocking(Dispatchers.IO) {
95+
dataStore.data.map { it[key] ?: defaultValue }.first()
96+
}
7797

7898
private fun <T> setValue(key: Preferences.Key<T>, value: T) {
79-
runBlocking { dataStore.edit { it[key] = value } }
99+
runBlocking(Dispatchers.IO) { dataStore.edit { it[key] = value } }
80100
}
81101

82102
private fun getEncryptedString(key: Preferences.Key<String>, defaultValue: String): String {
103+
if (isKeyInvalidated) return defaultValue
83104
val encrypted = getValue(key, "")
84105
if (encrypted.isEmpty()) return defaultValue
85106
val decrypted = encryption.decrypt(encrypted)
86-
return decrypted.ifEmpty { defaultValue }
107+
if (decrypted.isEmpty()) {
108+
// Encrypted data exists but decryption failed — keystore likely invalidated.
109+
// Clear corrupted data and force re-login.
110+
isKeyInvalidated = true
111+
scope.launch {
112+
clearCorePreferences()
113+
appNotifier.send(LogoutEvent(true))
114+
}
115+
return defaultValue
116+
}
117+
return decrypted
87118
}
88119

89120
private fun setEncryptedString(key: Preferences.Key<String>, value: String) {
@@ -148,7 +179,7 @@ class PreferencesManager(private val context: Context) :
148179
)
149180
}
150181
set(value) {
151-
runBlocking {
182+
runBlocking(Dispatchers.IO) {
152183
dataStore.edit { prefs ->
153184
prefs[Keys.VIDEO_SETTINGS_WIFI_DOWNLOAD_ONLY] = value.wifiDownloadOnly
154185
prefs[Keys.VIDEO_SETTINGS_STREAMING_QUALITY] = value.videoStreamingQuality.name
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package org.openedx.app.data.storage
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import androidx.datastore.core.DataMigration
6+
import androidx.datastore.preferences.core.MutablePreferences
7+
import androidx.datastore.preferences.core.Preferences
8+
import androidx.datastore.preferences.core.booleanPreferencesKey
9+
import androidx.datastore.preferences.core.longPreferencesKey
10+
import androidx.datastore.preferences.core.stringPreferencesKey
11+
import org.openedx.app.BuildConfig
12+
13+
class SharedPrefsToDataStoreMigration(
14+
private val context: Context,
15+
) : DataMigration<Preferences> {
16+
17+
private fun getOldPrefs(): SharedPreferences =
18+
context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE)
19+
20+
override suspend fun shouldMigrate(currentData: Preferences): Boolean =
21+
getOldPrefs().all.isNotEmpty()
22+
23+
override suspend fun migrate(currentData: Preferences): Preferences {
24+
val oldPrefs = getOldPrefs()
25+
val encryption = DataStoreEncryption()
26+
27+
return currentData.toMutablePreferences().apply {
28+
// Sensitive strings: encrypt during migration
29+
oldPrefs.migrateEncrypted(
30+
this,
31+
encryption,
32+
"access_token",
33+
stringPreferencesKey("access_token")
34+
)
35+
oldPrefs.migrateEncrypted(
36+
this,
37+
encryption,
38+
"refresh_token",
39+
stringPreferencesKey("refresh_token")
40+
)
41+
oldPrefs.migrateEncrypted(
42+
this,
43+
encryption,
44+
"push_token",
45+
stringPreferencesKey("push_token")
46+
)
47+
oldPrefs.migrateEncrypted(this, encryption, "user", stringPreferencesKey("user"))
48+
oldPrefs.migrateEncrypted(this, encryption, "account", stringPreferencesKey("account"))
49+
50+
// Long values (old key "CALENDAR_ID" → new key "calendar_id")
51+
oldPrefs.migrateLong(this, "expires_in", longPreferencesKey("expires_in"))
52+
oldPrefs.migrateLong(this, "CALENDAR_ID", longPreferencesKey("calendar_id"))
53+
54+
// Non-sensitive strings (old key "CALENDAR_USER" → new key "calendar_user")
55+
oldPrefs.migrateString(
56+
this,
57+
"video_settings_streaming_quality",
58+
stringPreferencesKey("video_settings_streaming_quality")
59+
)
60+
oldPrefs.migrateString(
61+
this,
62+
"video_settings_download_quality",
63+
stringPreferencesKey("video_settings_download_quality")
64+
)
65+
oldPrefs.migrateString(this, "app_config", stringPreferencesKey("app_config"))
66+
oldPrefs.migrateString(
67+
this,
68+
"last_whats_new_version",
69+
stringPreferencesKey("last_whats_new_version")
70+
)
71+
oldPrefs.migrateString(
72+
this,
73+
"last_review_version",
74+
stringPreferencesKey("last_review_version")
75+
)
76+
oldPrefs.migrateString(this, "CALENDAR_USER", stringPreferencesKey("calendar_user"))
77+
78+
// Booleans (some old keys were UPPER_CASE → new keys are lower_case)
79+
oldPrefs.migrateBoolean(
80+
this,
81+
"video_settings_wifi_download_only",
82+
booleanPreferencesKey("video_settings_wifi_download_only"),
83+
true
84+
)
85+
oldPrefs.migrateBoolean(
86+
this,
87+
"reset_app_directory",
88+
booleanPreferencesKey("reset_app_directory"),
89+
true
90+
)
91+
oldPrefs.migrateBoolean(
92+
this,
93+
"app_was_positive_rated",
94+
booleanPreferencesKey("app_was_positive_rated")
95+
)
96+
oldPrefs.migrateBoolean(
97+
this,
98+
"IS_RELATIVE_DATES_ENABLED",
99+
booleanPreferencesKey("is_relative_dates_enabled"),
100+
true
101+
)
102+
oldPrefs.migrateBoolean(
103+
this,
104+
"IS_CALENDAR_SYNC_ENABLED",
105+
booleanPreferencesKey("is_calendar_sync_enabled"),
106+
true
107+
)
108+
oldPrefs.migrateBoolean(
109+
this,
110+
"HIDE_INACTIVE_COURSES",
111+
booleanPreferencesKey("hide_inactive_courses"),
112+
true
113+
)
114+
}
115+
}
116+
117+
override suspend fun cleanUp() {
118+
getOldPrefs().edit().clear().commit()
119+
}
120+
121+
private fun SharedPreferences.migrateEncrypted(
122+
prefs: MutablePreferences,
123+
encryption: DataStoreEncryption,
124+
oldKey: String,
125+
newKey: Preferences.Key<String>,
126+
) {
127+
getString(oldKey, null)?.takeIf { it.isNotEmpty() }?.let {
128+
prefs[newKey] = encryption.encrypt(it)
129+
}
130+
}
131+
132+
private fun SharedPreferences.migrateString(
133+
prefs: MutablePreferences,
134+
oldKey: String,
135+
newKey: Preferences.Key<String>,
136+
) {
137+
getString(oldKey, null)?.let { prefs[newKey] = it }
138+
}
139+
140+
private fun SharedPreferences.migrateLong(
141+
prefs: MutablePreferences,
142+
oldKey: String,
143+
newKey: Preferences.Key<Long>,
144+
) {
145+
if (contains(oldKey)) prefs[newKey] = getLong(oldKey, 0L)
146+
}
147+
148+
private fun SharedPreferences.migrateBoolean(
149+
prefs: MutablePreferences,
150+
oldKey: String,
151+
newKey: Preferences.Key<Boolean>,
152+
defValue: Boolean = false,
153+
) {
154+
if (contains(oldKey)) prefs[newKey] = getBoolean(oldKey, defValue)
155+
}
156+
}

app/src/main/java/org/openedx/app/di/AppModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ import org.openedx.core.DatabaseManager as IDatabaseManager
8888
val appModule = module {
8989

9090
single { Config(get()) }
91-
single { PreferencesManager(get()) }
91+
single { PreferencesManager(get(), get()) }
9292
single<CorePreferences> { get<PreferencesManager>() }
9393
single<ProfilePreferences> { get<PreferencesManager>() }
9494
single<WhatsNewPreferences> { get<PreferencesManager>() }

0 commit comments

Comments
 (0)