Skip to content

Commit 09eda62

Browse files
authored
Merge pull request #387 from elimu-ai/375-export-to-csv
feat: export to csv
2 parents 28c550d + 26418c2 commit 09eda62

File tree

5 files changed

+91
-9
lines changed

5 files changed

+91
-9
lines changed

app/src/main/java/ai/elimu/analytics/dao/NumberAssessmentEventDao.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ interface NumberAssessmentEventDao {
1010
@Insert
1111
fun insert(numberAssessmentEvent: NumberAssessmentEvent)
1212

13+
@Query("SELECT * FROM NumberAssessmentEvent ORDER BY " +
14+
"CASE WHEN :isAsc = 1 THEN time END ASC," +
15+
"CASE WHEN :isAsc = 0 THEN time END DESC"
16+
)
17+
fun loadAllOrderedByTimestamp(isAsc: Boolean): List<NumberAssessmentEvent>
18+
1319
@Query("SELECT COUNT(*) FROM NumberAssessmentEvent")
1420
fun getCount(): Int
1521
}

app/src/main/java/ai/elimu/analytics/task/ExportEventsToCsvWorker.kt

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package ai.elimu.analytics.task
22

3+
import ai.elimu.analytics.BuildConfig
4+
import ai.elimu.analytics.db.RoomDb
35
import ai.elimu.analytics.db.getAllEvents
46
import ai.elimu.analytics.enum.EventType
57
import ai.elimu.analytics.entity.AssessmentEvent
68
import ai.elimu.analytics.entity.LearningEvent
79
import ai.elimu.analytics.enum.getCSVHeaders
810
import ai.elimu.analytics.enum.getUploadCsvFile
11+
import ai.elimu.analytics.util.SharedPreferencesHelper
912
import ai.elimu.analytics.util.VersionHelper.getAppVersionCode
1013
import android.content.Context
1114
import androidx.work.Worker
@@ -14,6 +17,7 @@ import org.apache.commons.csv.CSVFormat
1417
import org.apache.commons.csv.CSVPrinter
1518
import org.apache.commons.io.FileUtils
1619
import timber.log.Timber
20+
import java.io.File
1721
import java.io.IOException
1822
import java.io.StringWriter
1923
import java.text.SimpleDateFormat
@@ -34,6 +38,7 @@ class ExportEventsToCsvWorker(context: Context, workerParams: WorkerParameters)
3438
for (eventType in EventType.entries) {
3539
exportAnalyticsEventsToCsv(eventType = eventType)
3640
}
41+
exportNumberAssessmentEvents()
3742

3843
return Result.success()
3944
}
@@ -88,4 +93,69 @@ class ExportEventsToCsvWorker(context: Context, workerParams: WorkerParameters)
8893
Timber.e(e)
8994
}
9095
}
96+
97+
private fun exportNumberAssessmentEvents() {
98+
Timber.i("exportNumberAssessmentEvents")
99+
100+
// Read all the events from the database
101+
val roomDb = RoomDb.getDatabase(applicationContext)
102+
val numberAssessmentEventDao = roomDb.numberAssessmentEventDao()
103+
val events = numberAssessmentEventDao.loadAllOrderedByTimestamp(isAsc = true)
104+
Timber.i("events.size: ${events.size}")
105+
106+
// Generate one CSV file per day of events, e.g:
107+
// lang-THA/number-assessment-events/5b7c682a12ecbe2e_4000021_number-assessment-events_2025-06-29.csv
108+
// lang-THA/number-assessment-events/5b7c682a12ecbe2e_4000021_number-assessment-events_2025-06-30.csv
109+
var stringWriter: StringWriter? = null
110+
var csvPrinter: CSVPrinter? = null
111+
var dateOfPreviousEvent: String? = null
112+
for (event in events) {
113+
// Get the event's date in ISO format, e.g. "2025-06-29"
114+
val date: String = eventDateFormat.format(event.time.time)
115+
116+
// Prepare the CSV file path
117+
val languageDir = File(applicationContext.filesDir, "lang-${SharedPreferencesHelper.getLanguage(applicationContext)}")
118+
val eventsDir = File(languageDir, "number-assessment-events")
119+
val csvFile = File(eventsDir, "${event.androidId}_${BuildConfig.VERSION_CODE}_number-assessment-events_${date}.csv")
120+
121+
if (date != dateOfPreviousEvent) {
122+
// Reset file content, and prepare the headers for a new CSV file
123+
Timber.i("csvFile: ${csvFile}")
124+
stringWriter = StringWriter()
125+
csvPrinter = CSVPrinter(stringWriter, CSVFormat.DEFAULT.builder().setHeader(
126+
"id",
127+
"timestamp",
128+
"package_name",
129+
"mastery_score",
130+
"time_spent_ms",
131+
"additional_data",
132+
"research_experiment",
133+
"experiment_group",
134+
"number_value",
135+
"number_id"
136+
).get())
137+
}
138+
csvPrinter?.printRecord(
139+
event.id,
140+
event.time.timeInMillis / 1_000,
141+
event.packageName,
142+
event.masteryScore,
143+
event.timeSpentMs,
144+
event.additionalData,
145+
event.researchExperiment?.ordinal,
146+
event.experimentGroup?.ordinal,
147+
event.numberValue,
148+
event.numberId
149+
)
150+
csvPrinter?.flush()
151+
152+
// Write the content to the CSV file
153+
val csvFileContent = stringWriter.toString()
154+
FileUtils.writeStringToFile(csvFile, csvFileContent, "UTF-8")
155+
156+
dateOfPreviousEvent = date
157+
}
158+
159+
Timber.i("exportNumberAssessmentEvents complete!")
160+
}
91161
}

app/src/main/java/ai/elimu/analytics/task/TaskInitializer.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ object TaskInitializer {
1414
Timber.i("initializePeriodicWork")
1515

1616
// Periodically export events to CSV files
17-
val exportEventsToCsvWorkRequest = PeriodicWorkRequest.Builder(
18-
ExportEventsToCsvWorker::class.java, 15, TimeUnit.MINUTES
19-
)
17+
val exportEventsToCsvWorkRequest = PeriodicWorkRequest
18+
.Builder(ExportEventsToCsvWorker::class.java, 15, TimeUnit.MINUTES)
2019
.build()
2120
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
2221
"export_events_to_csv",
@@ -25,12 +24,12 @@ object TaskInitializer {
2524
)
2625

2726
// Periodically upload events (CSV files) to the server
28-
val uploadEventsConstraints = Constraints.Builder()
27+
val uploadEventsConstraints = Constraints
28+
.Builder()
2929
.setRequiredNetworkType(NetworkType.CONNECTED)
3030
.build()
31-
val uploadEventsWorkRequest = PeriodicWorkRequest.Builder(
32-
UploadEventsWorker::class.java, 3, TimeUnit.HOURS
33-
)
31+
val uploadEventsWorkRequest = PeriodicWorkRequest
32+
.Builder(UploadEventsWorker::class.java, 3, TimeUnit.HOURS)
3433
.setConstraints(uploadEventsConstraints)
3534
.build()
3635
WorkManager.getInstance(context).enqueueUniquePeriodicWork(

app/src/main/java/ai/elimu/analytics/util/SharedPreferencesHelper.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ object SharedPreferencesHelper {
2121

2222
@JvmStatic
2323
fun getAppVersionCode(context: Context): Int {
24-
Timber.i("getAppVersionCode")
2524
val sharedPreferences = context.getSharedPreferences(SHARED_PREFS, Context.MODE_PRIVATE)
2625
return sharedPreferences.getInt(PREF_APP_VERSION_CODE, 0)
2726
}
@@ -36,7 +35,6 @@ object SharedPreferencesHelper {
3635

3736
@JvmStatic
3837
fun getLanguage(context: Context): Language? {
39-
Timber.i("getLanguage")
4038
val sharedPreferences = context.getSharedPreferences(SHARED_PREFS, Context.MODE_PRIVATE)
4139
val languageAsString = sharedPreferences.getString(PREF_LANGUAGE, null)
4240
return if (TextUtils.isEmpty(languageAsString)) {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package ai.elimu.analytics.utils.enum
2+
3+
/**
4+
* Used for ordering data when querying DAOs and Content Providers.
5+
*/
6+
enum class SortOrder {
7+
ASC, // Ascending
8+
DESC // Descending
9+
}

0 commit comments

Comments
 (0)