diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt
index e0ce981..25cda63 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt
@@ -412,7 +412,10 @@ fun CalendarHost(
enter = slideInHorizontally(slideSpec) { it } + fadeIn(),
exit = slideOutHorizontally(slideSpec) { it } + fadeOut(),
) {
- CalendarsScreen(onBack = { showCalendars = false })
+ CalendarsScreen(
+ onBack = { showCalendars = false },
+ onImport = { importUri = it },
+ )
}
// Import flow for an opened/received .ics file. A single event routes
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt
index 9d087c6..3d82152 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt
@@ -37,6 +37,7 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FileDownload
+import androidx.compose.material.icons.filled.FileUpload
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.PhoneAndroid
@@ -112,6 +113,16 @@ import java.time.LocalDate
/** Sentinel [editorId] meaning "the editor is composing a new calendar". */
private const val NEW_CALENDAR_ID = Long.MIN_VALUE
+// SAF mime filter for the restore picker. `.ics` files reach us under several
+// mimes depending on the source app (our own export uses text/calendar; others
+// hand them out as octet-stream or text/plain), so accept the common set rather
+// than hide valid backups behind an over-tight filter.
+private val RESTORE_MIME_TYPES = arrayOf(
+ "text/calendar",
+ "application/octet-stream",
+ "text/plain",
+)
+
/**
* Calendar manager (reached from Settings). Lists the app's own device-only
* calendars with create / rename / recolor / delete (via a full-screen editor),
@@ -122,6 +133,7 @@ private const val NEW_CALENDAR_ID = Long.MIN_VALUE
@Composable
fun CalendarsScreen(
onBack: () -> Unit,
+ onImport: (android.net.Uri) -> Unit,
viewModel: CalendarsViewModel = hiltViewModel(),
) {
val calendars by viewModel.calendars.collectAsStateWithLifecycle()
@@ -168,6 +180,7 @@ fun CalendarsScreen(
onConsumeError = viewModel::consumeError,
backupResult = backupResult,
onExportBackup = viewModel::exportBackup,
+ onImport = onImport,
onConsumeBackupResult = viewModel::consumeBackupResult,
autoBackup = autoBackup,
onSetAutoBackupEnabled = viewModel::setAutoBackupEnabled,
@@ -191,6 +204,7 @@ private fun CalendarsList(
onConsumeError: () -> Unit,
backupResult: BackupResult?,
onExportBackup: (android.net.Uri) -> Unit,
+ onImport: (android.net.Uri) -> Unit,
onConsumeBackupResult: () -> Unit,
autoBackup: AutoBackupUiState,
onSetAutoBackupEnabled: (Boolean) -> Unit,
@@ -223,6 +237,13 @@ private fun CalendarsList(
contract = ActivityResultContracts.CreateDocument("text/calendar"),
) { uri -> uri?.let(onExportBackup) }
+ // SAF "open document" picker for restoring events from a .ics file. The
+ // picked Uri is handed up to the host, which runs it through the same import
+ // flow as an externally opened .ics (parse, dedup by UID, target picker).
+ val openBackup = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.OpenDocument(),
+ ) { uri -> uri?.let(onImport) }
+
// SAF folder picker for the automatic-backup destination; the VM persists the
// write grant so background runs can keep writing to it.
val pickFolder = rememberLauncherForActivityResult(
@@ -316,6 +337,15 @@ private fun CalendarsList(
runCatching { createBackup.launch("calendula-backup-${LocalDate.now()}.ics") }
},
)
+ GroupedRow(
+ title = stringResource(R.string.calendars_restore_action),
+ summary = stringResource(R.string.calendars_restore_hint),
+ position = Position.Middle,
+ leading = { LeadingAvatar(Icons.Default.FileUpload) },
+ onClick = {
+ runCatching { openBackup.launch(RESTORE_MIME_TYPES) }
+ },
+ )
GroupedRow(
title = stringResource(R.string.calendars_auto_backup),
summary = stringResource(R.string.calendars_auto_backup_hint),
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9c9e8df..d3272e7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -428,6 +428,8 @@
Backup
Local calendars aren\'t synced anywhere, so export them to an .ics file to keep a copy.
Export as .ics file
+ Restore from .ics file
+ Import events from a backup or another calendar app.
Automatic backup
Periodically export your local calendars to a folder as an .ics file.
Backup folder