Skip to content

Commit f82ab19

Browse files
committed
feat: use SAF file picker for swipe data export buttons
- Export JSON and Export NDJSON buttons now open file picker - Matches behavior of config/clipboard backup export buttons - Added exportToJSON(OutputStream) and exportToNDJSON(OutputStream) methods - Default filename includes timestamp (swipe_data_YYYYMMDD_HHMMSS.json) — claude-opus-4-5-20251101
1 parent 17b0d30 commit f82ab19

2 files changed

Lines changed: 113 additions & 15 deletions

File tree

src/main/kotlin/tribixbite/cleverkeys/SettingsActivity.kt

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,19 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
100100
uri?.let { performClipboardImport(it) }
101101
}
102102

103+
// SAF file pickers for swipe ML data export
104+
private val swipeDataJsonExportLauncher = registerForActivityResult(
105+
ActivityResultContracts.CreateDocument("application/json")
106+
) { uri: Uri? ->
107+
uri?.let { performSwipeDataJsonExport(it) }
108+
}
109+
110+
private val swipeDataNdjsonExportLauncher = registerForActivityResult(
111+
ActivityResultContracts.CreateDocument("application/x-ndjson")
112+
) { uri: Uri? ->
113+
uri?.let { performSwipeDataNdjsonExport(it) }
114+
}
115+
103116
// Settings state for reactive UI
104117
private var beamWidth by mutableStateOf(6)
105118
private var maxLength by mutableStateOf(20)
@@ -3448,15 +3461,37 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
34483461
}
34493462

34503463
private fun exportSwipeDataJSON() {
3464+
try {
3465+
val sdf = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.US)
3466+
val filename = "swipe_data_${sdf.format(java.util.Date())}.json"
3467+
swipeDataJsonExportLauncher.launch(filename)
3468+
} catch (e: Exception) {
3469+
Toast.makeText(this, "Could not open file picker: ${e.message}", Toast.LENGTH_SHORT).show()
3470+
}
3471+
}
3472+
3473+
private fun exportSwipeDataNDJSON() {
3474+
try {
3475+
val sdf = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.US)
3476+
val filename = "swipe_data_${sdf.format(java.util.Date())}.ndjson"
3477+
swipeDataNdjsonExportLauncher.launch(filename)
3478+
} catch (e: Exception) {
3479+
Toast.makeText(this, "Could not open file picker: ${e.message}", Toast.LENGTH_SHORT).show()
3480+
}
3481+
}
3482+
3483+
private fun performSwipeDataJsonExport(uri: Uri) {
34513484
lifecycleScope.launch {
34523485
try {
3453-
val dataStore = tribixbite.cleverkeys.ml.SwipeMLDataStore.getInstance(this@SettingsActivity)
3454-
val exportFile = dataStore.exportToJSON()
3455-
Toast.makeText(
3456-
this@SettingsActivity,
3457-
"Exported to: ${exportFile.absolutePath}",
3458-
Toast.LENGTH_LONG
3459-
).show()
3486+
contentResolver.openOutputStream(uri)?.use { outputStream ->
3487+
val dataStore = tribixbite.cleverkeys.ml.SwipeMLDataStore.getInstance(this@SettingsActivity)
3488+
val count = dataStore.exportToJSON(outputStream)
3489+
Toast.makeText(
3490+
this@SettingsActivity,
3491+
"Exported $count swipe entries to JSON",
3492+
Toast.LENGTH_SHORT
3493+
).show()
3494+
} ?: throw Exception("Could not open file for writing")
34603495
} catch (e: Exception) {
34613496
Toast.makeText(
34623497
this@SettingsActivity,
@@ -3467,16 +3502,18 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
34673502
}
34683503
}
34693504

3470-
private fun exportSwipeDataNDJSON() {
3505+
private fun performSwipeDataNdjsonExport(uri: Uri) {
34713506
lifecycleScope.launch {
34723507
try {
3473-
val dataStore = tribixbite.cleverkeys.ml.SwipeMLDataStore.getInstance(this@SettingsActivity)
3474-
val exportFile = dataStore.exportToNDJSON()
3475-
Toast.makeText(
3476-
this@SettingsActivity,
3477-
"Exported to: ${exportFile.absolutePath}",
3478-
Toast.LENGTH_LONG
3479-
).show()
3508+
contentResolver.openOutputStream(uri)?.use { outputStream ->
3509+
val dataStore = tribixbite.cleverkeys.ml.SwipeMLDataStore.getInstance(this@SettingsActivity)
3510+
val count = dataStore.exportToNDJSON(outputStream)
3511+
Toast.makeText(
3512+
this@SettingsActivity,
3513+
"Exported $count swipe entries to NDJSON",
3514+
Toast.LENGTH_SHORT
3515+
).show()
3516+
} ?: throw Exception("Could not open file for writing")
34803517
} catch (e: Exception) {
34813518
Toast.makeText(
34823519
this@SettingsActivity,

src/main/kotlin/tribixbite/cleverkeys/ml/SwipeMLDataStore.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import java.io.BufferedReader
1111
import java.io.File
1212
import java.io.FileReader
1313
import java.io.FileWriter
14+
import java.io.OutputStream
15+
import java.io.OutputStreamWriter
1416
import java.text.SimpleDateFormat
1517
import java.util.Date
1618
import java.util.Locale
@@ -320,6 +322,65 @@ class SwipeMLDataStore private constructor(context: Context) :
320322
return exportFile
321323
}
322324

325+
/**
326+
* Export all data to JSON via OutputStream (for SAF file picker)
327+
*/
328+
fun exportToJSON(outputStream: OutputStream): Int {
329+
val allData = loadAllData()
330+
331+
// Build JSON array
332+
val jsonArray = JSONArray()
333+
for (data in allData) {
334+
jsonArray.put(data.toJSON())
335+
}
336+
337+
// Add metadata
338+
val root = JSONObject().apply {
339+
put("export_version", "1.0")
340+
put("export_timestamp", System.currentTimeMillis())
341+
put("total_samples", allData.size)
342+
put("database_version", DATABASE_VERSION)
343+
put("data", jsonArray)
344+
}
345+
346+
// Add statistics
347+
val prefs = _context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
348+
val stats = JSONObject().apply {
349+
put("total_swipes", prefs.getInt(PREF_TOTAL_COUNT, 0))
350+
put("calibration_swipes", prefs.getInt(PREF_CALIBRATION_COUNT, 0))
351+
put("user_swipes", prefs.getInt(PREF_USER_COUNT, 0))
352+
}
353+
root.put("statistics", stats)
354+
355+
// Write to stream
356+
OutputStreamWriter(outputStream, Charsets.UTF_8).use { writer ->
357+
writer.write(root.toString(2)) // Pretty print with 2-space indent
358+
}
359+
360+
// Mark all as exported
361+
markAllAsExported()
362+
363+
Log.i(TAG, "Exported ${allData.size} entries to JSON stream")
364+
return allData.size
365+
}
366+
367+
/**
368+
* Export to NDJSON via OutputStream (for SAF file picker)
369+
*/
370+
fun exportToNDJSON(outputStream: OutputStream): Int {
371+
val allData = loadAllData()
372+
373+
OutputStreamWriter(outputStream, Charsets.UTF_8).use { writer ->
374+
for (data in allData) {
375+
writer.write(data.toJSON().toString())
376+
writer.write("\n")
377+
}
378+
}
379+
380+
Log.i(TAG, "Exported ${allData.size} entries to NDJSON stream")
381+
return allData.size
382+
}
383+
323384
/**
324385
* Get statistics about stored data
325386
*/

0 commit comments

Comments
 (0)