579 lines
17 KiB
Markdown
579 lines
17 KiB
Markdown
# Vocabulary Export/Import System
|
|
|
|
## Overview
|
|
|
|
The Polly app includes a comprehensive vocabulary export/import system that allows users to:
|
|
- **Backup** their complete vocabulary repository
|
|
- **Share** vocabulary lists with friends, teachers, or students
|
|
- **Transfer** data between devices
|
|
- **Exchange** vocabulary via messaging apps (WhatsApp, Telegram, etc.)
|
|
- **Store** vocabulary in cloud services (Google Drive, Dropbox, etc.)
|
|
- **Integrate** with external systems via REST APIs
|
|
|
|
## Data Format
|
|
|
|
The export/import system uses **JSON** as the primary data format. JSON was chosen because it is:
|
|
- **Text-based**: Can be shared via any text-based communication channel
|
|
- **Portable**: Works across all platforms and devices
|
|
- **Human-readable**: Can be inspected and edited manually if needed
|
|
- **Standard**: Supported by all programming languages and APIs
|
|
- **Compact**: Efficient storage and transmission
|
|
|
|
## Architecture
|
|
|
|
### Core Components
|
|
|
|
1. **VocabularyExport.kt**: Defines data models for export/import
|
|
2. **VocabularyRepository.kt**: Implements export/import functions
|
|
3. **ConflictStrategy**: Defines how to handle data conflicts during import
|
|
|
|
### Data Models
|
|
|
|
The system uses a sealed class hierarchy for different export scopes:
|
|
|
|
```kotlin
|
|
sealed class VocabularyExportData {
|
|
abstract val formatVersion: Int
|
|
abstract val exportDate: Instant
|
|
abstract val metadata: ExportMetadata
|
|
}
|
|
```
|
|
|
|
#### Export Types
|
|
|
|
1. **FullRepositoryExport**: Complete backup of everything
|
|
- All vocabulary items
|
|
- All categories (tags and filters)
|
|
- All learning states
|
|
- All category mappings
|
|
- All stage mappings
|
|
|
|
2. **CategoryExport**: Single category with its items
|
|
- One category definition
|
|
- All items in that category
|
|
- Learning states for those items
|
|
- Stage mappings for those items
|
|
|
|
3. **ItemListExport**: Custom selection of items
|
|
- Selected vocabulary items
|
|
- Learning states for those items
|
|
- Stage mappings for those items
|
|
- Optionally: associated categories
|
|
|
|
4. **SingleItemExport**: Individual vocabulary item
|
|
- One vocabulary item
|
|
- Its learning state
|
|
- Its current stage
|
|
- Categories it belongs to
|
|
|
|
## Usage Guide
|
|
|
|
### Exporting Data
|
|
|
|
#### 1. Export Full Repository
|
|
|
|
```kotlin
|
|
// In a coroutine scope
|
|
val repository = VocabularyRepository.getInstance(context)
|
|
|
|
// Create export data
|
|
val exportData = repository.exportFullRepository()
|
|
|
|
// Convert to JSON string
|
|
val jsonString = repository.exportToJson(exportData, prettyPrint = true)
|
|
|
|
// Save to file, share, or upload
|
|
saveToFile(jsonString, "vocabulary_backup.json")
|
|
```
|
|
|
|
#### 2. Export Single Category
|
|
|
|
```kotlin
|
|
val categoryId = 123
|
|
val exportData = repository.exportCategory(categoryId)
|
|
|
|
if (exportData != null) {
|
|
val jsonString = repository.exportToJson(exportData)
|
|
shareViaIntent(jsonString)
|
|
} else {
|
|
// Category not found
|
|
}
|
|
```
|
|
|
|
#### 3. Export Custom Item List
|
|
|
|
```kotlin
|
|
val itemIds = listOf(1, 5, 10, 15, 20)
|
|
val exportData = repository.exportItemList(itemIds, includeCategories = true)
|
|
val jsonString = repository.exportToJson(exportData)
|
|
```
|
|
|
|
#### 4. Export Single Item
|
|
|
|
```kotlin
|
|
val itemId = 42
|
|
val exportData = repository.exportSingleItem(itemId)
|
|
|
|
if (exportData != null) {
|
|
val jsonString = repository.exportToJson(exportData)
|
|
// Share via WhatsApp, email, etc.
|
|
}
|
|
```
|
|
|
|
### Importing Data
|
|
|
|
#### 1. Import from JSON String
|
|
|
|
```kotlin
|
|
// Receive JSON string (from file, intent, API, etc.)
|
|
val jsonString = readFromFile("vocabulary_backup.json")
|
|
|
|
// Parse JSON
|
|
val exportData = repository.importFromJson(jsonString)
|
|
|
|
// Import with conflict strategy
|
|
val result = repository.importVocabularyData(
|
|
exportData = exportData,
|
|
strategy = ConflictStrategy.MERGE
|
|
)
|
|
|
|
// Check result
|
|
if (result.isSuccess) {
|
|
println("Imported: ${result.itemsImported} items")
|
|
println("Skipped: ${result.itemsSkipped} items")
|
|
println("Categories: ${result.categoriesImported}")
|
|
} else {
|
|
println("Errors: ${result.errors}")
|
|
}
|
|
```
|
|
|
|
### Conflict Resolution Strategies
|
|
|
|
When importing data, you must choose how to handle conflicts (duplicate items or categories):
|
|
|
|
#### 1. SKIP Strategy
|
|
```kotlin
|
|
strategy = ConflictStrategy.SKIP
|
|
```
|
|
- **Behavior**: Skip importing items that already exist
|
|
- **Use case**: Importing shared vocabulary without overwriting your progress
|
|
- **Result**: Preserves all existing data unchanged
|
|
|
|
#### 2. REPLACE Strategy
|
|
```kotlin
|
|
strategy = ConflictStrategy.REPLACE
|
|
```
|
|
- **Behavior**: Replace existing items with imported versions
|
|
- **Use case**: Restoring from backup, syncing with authoritative source
|
|
- **Result**: Overwrites local data with imported data
|
|
|
|
#### 3. MERGE Strategy (Default)
|
|
```kotlin
|
|
strategy = ConflictStrategy.MERGE
|
|
```
|
|
- **Behavior**: Intelligently merge data
|
|
- For items: Keep existing if duplicate, add new ones
|
|
- For states: Keep the more advanced learning progress
|
|
- For stages: Keep the higher stage
|
|
- For categories: Merge memberships
|
|
- **Use case**: Most common scenario, combining data from multiple sources
|
|
- **Result**: Best of both worlds
|
|
|
|
#### 4. RENAME Strategy
|
|
```kotlin
|
|
strategy = ConflictStrategy.RENAME
|
|
```
|
|
- **Behavior**: Assign new IDs to all imported items
|
|
- **Use case**: Intentionally creating duplicates for practice
|
|
- **Result**: All imported items get new IDs, no conflicts
|
|
|
|
## Data Preservation
|
|
|
|
### What Gets Exported
|
|
|
|
Every export includes complete information:
|
|
|
|
1. **Vocabulary Items**
|
|
- Word/phrase in first language
|
|
- Word/phrase in second language
|
|
- Language IDs
|
|
- Creation timestamp
|
|
- Grammatical features (if any)
|
|
- Zipf frequency scores (if available)
|
|
|
|
2. **Learning States**
|
|
- Correct answer count
|
|
- Incorrect answer count
|
|
- Last correct answer timestamp
|
|
- Last incorrect answer timestamp
|
|
|
|
3. **Stage Mappings**
|
|
- Current learning stage (NEW, STAGE_1-5, LEARNED)
|
|
- For each vocabulary item
|
|
|
|
4. **Categories**
|
|
- Category name and type
|
|
- For TagCategory: just the name
|
|
- For VocabularyFilter: language filters, stage filters, language pairs
|
|
|
|
5. **Category Memberships**
|
|
- Which items belong to which categories
|
|
- Automatically recalculated for filters during import
|
|
|
|
### Metadata
|
|
|
|
Each export includes metadata:
|
|
- Format version (for future compatibility)
|
|
- Export date/time
|
|
- Item count
|
|
- Category count
|
|
- Export scope description
|
|
- App version (optional)
|
|
|
|
## Integration Examples
|
|
|
|
### 1. File Storage
|
|
|
|
```kotlin
|
|
// Save to device storage
|
|
fun saveVocabularyToFile(context: Context, exportData: VocabularyExportData) {
|
|
val jsonString = repository.exportToJson(exportData, prettyPrint = true)
|
|
val file = File(context.getExternalFilesDir(null), "vocabulary_export.json")
|
|
file.writeText(jsonString)
|
|
}
|
|
|
|
// Load from device storage
|
|
fun loadVocabularyFromFile(context: Context): ImportResult {
|
|
val file = File(context.getExternalFilesDir(null), "vocabulary_export.json")
|
|
val jsonString = file.readText()
|
|
val exportData = repository.importFromJson(jsonString)
|
|
return repository.importVocabularyData(exportData, ConflictStrategy.MERGE)
|
|
}
|
|
```
|
|
|
|
### 2. Share via Intent (WhatsApp, Email, etc.)
|
|
|
|
```kotlin
|
|
fun shareVocabulary(context: Context, exportData: VocabularyExportData) {
|
|
val jsonString = repository.exportToJson(exportData)
|
|
|
|
val sendIntent = Intent().apply {
|
|
action = Intent.ACTION_SEND
|
|
putExtra(Intent.EXTRA_TEXT, jsonString)
|
|
putExtra(Intent.EXTRA_SUBJECT, "Vocabulary List: ${exportData.metadata.exportScope}")
|
|
type = "text/plain"
|
|
}
|
|
|
|
context.startActivity(Intent.createChooser(sendIntent, "Share vocabulary"))
|
|
}
|
|
|
|
// Receive from intent
|
|
fun receiveVocabulary(intent: Intent): ImportResult? {
|
|
val jsonString = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return null
|
|
val exportData = repository.importFromJson(jsonString)
|
|
return repository.importVocabularyData(exportData, ConflictStrategy.MERGE)
|
|
}
|
|
```
|
|
|
|
### 3. REST API Integration
|
|
|
|
```kotlin
|
|
// Upload to server
|
|
suspend fun uploadToServer(exportData: VocabularyExportData): Result<String> {
|
|
val jsonString = repository.exportToJson(exportData)
|
|
|
|
val client = HttpClient()
|
|
val response = client.post("https://api.example.com/vocabulary") {
|
|
contentType(ContentType.Application.Json)
|
|
setBody(jsonString)
|
|
}
|
|
|
|
return if (response.status.isSuccess()) {
|
|
Result.success(response.body())
|
|
} else {
|
|
Result.failure(Exception("Upload failed"))
|
|
}
|
|
}
|
|
|
|
// Download from server
|
|
suspend fun downloadFromServer(vocabularyId: String): ImportResult {
|
|
val client = HttpClient()
|
|
val jsonString = client.get("https://api.example.com/vocabulary/$vocabularyId").body<String>()
|
|
|
|
val exportData = repository.importFromJson(jsonString)
|
|
return repository.importVocabularyData(exportData, ConflictStrategy.MERGE)
|
|
}
|
|
```
|
|
|
|
### 4. Cloud Storage (Google Drive, Dropbox)
|
|
|
|
```kotlin
|
|
// Upload to Google Drive
|
|
fun uploadToGoogleDrive(driveService: Drive, exportData: VocabularyExportData): String {
|
|
val jsonString = repository.exportToJson(exportData, prettyPrint = true)
|
|
|
|
val fileMetadata = File().apply {
|
|
name = "polly_vocabulary_${System.currentTimeMillis()}.json"
|
|
mimeType = "application/json"
|
|
}
|
|
|
|
val content = ByteArrayContent.fromString("application/json", jsonString)
|
|
val file = driveService.files().create(fileMetadata, content).execute()
|
|
|
|
return file.id
|
|
}
|
|
|
|
// Download from Google Drive
|
|
fun downloadFromGoogleDrive(driveService: Drive, fileId: String): ImportResult {
|
|
val outputStream = ByteArrayOutputStream()
|
|
driveService.files().get(fileId).executeMediaAndDownloadTo(outputStream)
|
|
|
|
val jsonString = outputStream.toString("UTF-8")
|
|
val exportData = repository.importFromJson(jsonString)
|
|
return repository.importVocabularyData(exportData, ConflictStrategy.MERGE)
|
|
}
|
|
```
|
|
|
|
### 5. QR Code Sharing
|
|
|
|
```kotlin
|
|
// Generate QR code for small exports
|
|
fun generateQRCode(exportData: VocabularyExportData): Bitmap {
|
|
val jsonString = repository.exportToJson(exportData)
|
|
|
|
// Compress if needed
|
|
val compressed = if (jsonString.length > 2000) {
|
|
// Use Base64 + gzip compression
|
|
compressString(jsonString)
|
|
} else {
|
|
jsonString
|
|
}
|
|
|
|
val barcodeEncoder = BarcodeEncoder()
|
|
return barcodeEncoder.encodeBitmap(compressed, BarcodeFormat.QR_CODE, 512, 512)
|
|
}
|
|
|
|
// Scan QR code
|
|
fun scanQRCode(qrContent: String): ImportResult {
|
|
val jsonString = if (isCompressed(qrContent)) {
|
|
decompressString(qrContent)
|
|
} else {
|
|
qrContent
|
|
}
|
|
|
|
val exportData = repository.importFromJson(jsonString)
|
|
return repository.importVocabularyData(exportData, ConflictStrategy.MERGE)
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Common Errors
|
|
|
|
1. **Invalid JSON Format**
|
|
```kotlin
|
|
try {
|
|
val exportData = repository.importFromJson(jsonString)
|
|
} catch (e: SerializationException) {
|
|
// Invalid JSON format
|
|
Log.e(TAG, "Failed to parse JSON: ${e.message}")
|
|
}
|
|
```
|
|
|
|
2. **Import Failures**
|
|
```kotlin
|
|
val result = repository.importVocabularyData(exportData, strategy)
|
|
if (!result.isSuccess) {
|
|
result.errors.forEach { error ->
|
|
Log.e(TAG, "Import error: $error")
|
|
}
|
|
}
|
|
```
|
|
|
|
3. **Version Compatibility**
|
|
```kotlin
|
|
if (exportData.formatVersion > CURRENT_FORMAT_VERSION) {
|
|
// Warn user that format is from newer app version
|
|
showWarning("This export was created with a newer version of the app")
|
|
}
|
|
```
|
|
|
|
## Performance Considerations
|
|
|
|
### Large Exports
|
|
|
|
For repositories with thousands of items:
|
|
|
|
1. **Chunked Processing**: Process items in batches
|
|
2. **Background Thread**: Use coroutines with Dispatchers.IO
|
|
3. **Progress Reporting**: Update UI during long operations
|
|
4. **Compression**: Use gzip for large JSON files
|
|
|
|
```kotlin
|
|
suspend fun importLargeExport(jsonString: String, onProgress: (Int, Int) -> Unit): ImportResult {
|
|
return withContext(Dispatchers.IO) {
|
|
val exportData = repository.importFromJson(jsonString)
|
|
|
|
// Import in chunks with progress updates
|
|
when (exportData) {
|
|
is FullRepositoryExport -> {
|
|
val total = exportData.items.size
|
|
var processed = 0
|
|
|
|
exportData.items.chunked(100).forEach { chunk ->
|
|
// Process chunk
|
|
processed += chunk.size
|
|
onProgress(processed, total)
|
|
}
|
|
}
|
|
// Handle other types...
|
|
}
|
|
|
|
repository.importVocabularyData(exportData, ConflictStrategy.MERGE)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Unit Tests
|
|
|
|
Test export/import roundtrip:
|
|
|
|
```kotlin
|
|
@Test
|
|
fun testExportImportRoundtrip() = runBlocking {
|
|
// Create test data
|
|
val originalItems = listOf(
|
|
VocabularyItem(1, 1, 2, "hello", "hola", Clock.System.now())
|
|
)
|
|
repository.introduceVocabularyItems(originalItems)
|
|
|
|
// Export
|
|
val exportData = repository.exportFullRepository()
|
|
val jsonString = repository.exportToJson(exportData)
|
|
|
|
// Clear repository
|
|
repository.wipeRepository()
|
|
|
|
// Import
|
|
val importData = repository.importFromJson(jsonString)
|
|
val result = repository.importVocabularyData(importData, ConflictStrategy.MERGE)
|
|
|
|
// Verify
|
|
assertEquals(1, result.itemsImported)
|
|
val importedItems = repository.getAllVocabularyItems()
|
|
assertEquals(originalItems.size, importedItems.size)
|
|
}
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
Test with external storage:
|
|
|
|
```kotlin
|
|
@Test
|
|
fun testFileExportImport() = runBlocking {
|
|
// Export to file
|
|
val exportData = repository.exportFullRepository()
|
|
val jsonString = repository.exportToJson(exportData)
|
|
val file = File.createTempFile("vocab", ".json")
|
|
file.writeText(jsonString)
|
|
|
|
// Import from file
|
|
val importedJson = file.readText()
|
|
val importData = repository.importFromJson(importedJson)
|
|
val result = repository.importVocabularyData(importData, ConflictStrategy.REPLACE)
|
|
|
|
// Verify
|
|
assertTrue(result.isSuccess)
|
|
}
|
|
```
|
|
|
|
## Future Enhancements
|
|
|
|
### Potential Improvements
|
|
|
|
1. **Compression**: Add built-in gzip compression for large exports
|
|
2. **Encryption**: Support for encrypted exports with password protection
|
|
3. **Incremental Sync**: Export only changes since last sync
|
|
4. **Conflict Resolution UI**: Let users manually resolve conflicts
|
|
5. **Batch Operations**: Import multiple exports in one operation
|
|
6. **Export Templates**: Pre-defined export configurations
|
|
7. **Automatic Backups**: Scheduled background exports
|
|
8. **Cloud Sync**: Automatic bidirectional synchronization
|
|
9. **Format Migration**: Automatic upgrades from older format versions
|
|
10. **Validation**: Pre-import validation with detailed reports
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
**Q: Import says "0 items imported" but no errors**
|
|
- A: All items were duplicates and SKIP strategy was used
|
|
- Solution: Use MERGE or REPLACE strategy
|
|
|
|
**Q: Categories missing after import**
|
|
- A: Only TagCategories are imported; VocabularyFilters are recreated automatically
|
|
- Solution: This is by design; filters regenerate based on rules
|
|
|
|
**Q: Learning progress lost after import**
|
|
- A: REPLACE strategy was used, overwriting existing progress
|
|
- Solution: Use MERGE strategy to preserve better progress
|
|
|
|
**Q: JSON file too large to share via WhatsApp**
|
|
- A: Large repositories exceed message size limits
|
|
- Solution: Use file sharing, cloud storage, or export specific categories
|
|
|
|
**Q: Import fails with "Invalid JSON"**
|
|
- A: JSON was corrupted or manually edited incorrectly
|
|
- Solution: Ensure JSON is valid; don't manually edit unless necessary
|
|
|
|
## Best Practices
|
|
|
|
1. **Regular Backups**: Export full repository regularly
|
|
2. **Test Imports**: Test import in a fresh profile before overwriting
|
|
3. **Use MERGE**: Default to MERGE strategy for most use cases
|
|
4. **Validate Data**: Check ImportResult after each import
|
|
5. **Keep Metadata**: Don't remove metadata from exported JSON
|
|
6. **Version Tracking**: Include app version in exports
|
|
7. **Compression**: Compress large exports before sharing
|
|
8. **Secure Exports**: Be cautious with exports containing sensitive data
|
|
9. **Document Changes**: Add notes about what was exported/imported
|
|
10. **Incremental Sharing**: Share specific categories instead of full repo
|
|
|
|
## API Reference
|
|
|
|
### Repository Functions
|
|
|
|
#### Export Functions
|
|
|
|
- `exportFullRepository(): FullRepositoryExport`
|
|
- `exportCategory(categoryId: Int): CategoryExport?`
|
|
- `exportItemList(itemIds: List<Int>, includeCategories: Boolean = true): ItemListExport`
|
|
- `exportSingleItem(itemId: Int): SingleItemExport?`
|
|
- `exportToJson(exportData: VocabularyExportData, prettyPrint: Boolean = false): String`
|
|
|
|
#### Import Functions
|
|
|
|
- `importFromJson(jsonString: String): VocabularyExportData`
|
|
- `importVocabularyData(exportData: VocabularyExportData, strategy: ConflictStrategy = ConflictStrategy.MERGE): ImportResult`
|
|
|
|
### Data Classes
|
|
|
|
- `ExportMetadata`: Information about the export
|
|
- `ImportResult`: Statistics and errors from import
|
|
- `ConflictStrategy`: Enum defining conflict resolution behavior
|
|
- `CategoryMappingData`: Item-to-category relationship
|
|
- `StageMappingData`: Item-to-stage relationship
|
|
|
|
## Conclusion
|
|
|
|
The vocabulary export/import system provides a robust, flexible solution for data portability in the Polly app. Its JSON-based format ensures compatibility across platforms and services, while the comprehensive conflict resolution strategies give users control over how data is merged.
|
|
|
|
Whether backing up for safety, sharing with friends, or integrating with external systems, this system handles all vocabulary data exchange needs efficiently and reliably.
|
|
|
|
---
|
|
|
|
*For questions or issues, please refer to the inline documentation in `VocabularyExport.kt` and `VocabularyRepository.kt`.*
|