Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions app/src/main/java/com/gatecontrol/android/drive/DriveApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,26 @@ class DriveApi @Inject constructor(
*/
suspend fun deadlineToCalendar(id: Long): DResult<DDeadline> = post("deadlines/$id/calendar", JsonObject(), DriveJson::deadlineAnswer)

/**
* Deadline [id] into [target] (Drive 1.17, feature `deadlineMailCalendar`): a mail calendar, else the family
* calendar → the deadline with `calendar`; the mail calendar not reachable: `mail_unavailable`.
*/
suspend fun deadlineToCalendar(id: Long, target: DDeadlineTarget?): DResult<DDeadline> =
post("deadlines/$id/calendar", DriveJson.deadlineCalendarBody(target), DriveJson::deadlineAnswer)

/** The calendars a deadline can go to (Drive 1.17): the family calendar, and my mail calendars. */
suspend fun deadlineCalendars(): DResult<DDeadlineCalendars> = get("deadlines/calendars", parse = DriveJson::deadlineCalendars)

/** My target per area for new deadlines (Drive 1.17; the areas with AI for documents). */
suspend fun deadlineTargets(): DResult<List<DDeadlineTargetRow>> = get("deadlines/targets", parse = DriveJson::deadlineTargets)

/** My target of area [id] for new deadlines (null: off) → my targets now. */
suspend fun setDeadlineTarget(id: String, target: DDeadlineTarget?): DResult<List<DDeadlineTargetRow>> {
val area = DriveJson.areaId(id) ?: return bad()
if (target?.mail == true && target.calendarId.isNullOrEmpty()) return bad()
return put("deadlines/targets", DriveJson.deadlineTargetsBody(area, target), DriveJson::deadlineTargets)
}

/** AI for documents in area [id] on or off (`PATCH areas/<id> { docAi }`, the write right) → the setting now. */
suspend fun setAreaDocAi(id: String, on: Boolean): DResult<Boolean> {
val area = DriveJson.areaId(id) ?: return bad()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.gatecontrol.android.drive

// ─── Drive 1.17: deadlines into a calendar of the mail plugin too (feature `deadlineMailCalendar`) ──

/**
* Where a deadline goes into a calendar: [KIND_FAMILY] (the family calendar) or [KIND_MAIL] (a calendar [calendarId] of
* the mail plugin, e.g. Outlook). No target (off) is `null`. [label] is the server's text of it ("Outlook · Kalender").
*/
data class DDeadlineTarget(val kind: String, val calendarId: String? = null, val label: String? = null) {
val family: Boolean get() = kind == KIND_FAMILY
val mail: Boolean get() = kind == KIND_MAIL

/** The same calendar (the label does not count). */
fun sameAs(other: DDeadlineTarget?): Boolean = other != null && kind == other.kind && (family || calendarId == other.calendarId)

companion object {
const val KIND_FAMILY = "family"
const val KIND_MAIL = "mail"
val FAMILY = DDeadlineTarget(KIND_FAMILY)
}
}

/** An account of the mail plugin ([kind] e.g. `microsoft`, `caldav`, `local`). */
data class DMailAccount(val id: String, val label: String, val kind: String?)

/** A calendar of the mail plugin the person may write to (`GET deadlines/calendars`); [color] `#rrggbb`. */
data class DMailCalendar(val id: String, val name: String, val account: DMailAccount, val color: String?)

/** `GET deadlines/calendars`: whether the family calendar takes entries, and the person's mail calendars. */
data class DDeadlineCalendars(val family: Boolean, val mail: List<DMailCalendar>) {
companion object {
val NONE = DDeadlineCalendars(family = false, mail = emptyList())
}
}

/** `GET deadlines/targets`: my target for the deadlines of area [areaId] ([label] its name); null: off. */
data class DDeadlineTargetRow(val areaId: String, val label: String, val target: DDeadlineTarget?)

/** What the settings and the deadlines offer: the choices, grouped like the dialog shows them, and the texts of a target. */
object DriveDeadlineTargets {

/** One line of the choice: off, the family calendar or a mail calendar. */
data class Choice(val target: DDeadlineTarget?, val calendar: DMailCalendar? = null)

/** A block of the choice: mail calendars under their [account] (null: off and the family calendar, no heading). */
data class Group(val account: DMailAccount?, val choices: List<Choice>)

/**
* The choice of an area's target: "Nicht eintragen" ([off]), the family calendar (when Familie takes entries), then
* the mail calendars by account (only with [mailFeature]), in the server's order.
*/
fun groups(calendars: DDeadlineCalendars, mailFeature: Boolean, off: Boolean = true): List<Group> {
val first = buildList {
if (off) add(Choice(null))
if (calendars.family) add(Choice(DDeadlineTarget.FAMILY))
}
val mail = if (!mailFeature) emptyList() else calendars.mail.groupBy { it.account.id }.values.map { list ->
Group(list.first().account, list.map { c -> Choice(mailTarget(c), c) })
}
return (if (first.isEmpty()) emptyList() else listOf(Group(null, first))) + mail
}

/** The targets a single deadline can go to (no "off"). */
fun targets(calendars: DDeadlineCalendars, mailFeature: Boolean): List<DDeadlineTarget> =
groups(calendars, mailFeature, off = false).flatMap { g -> g.choices.mapNotNull { it.target } }

/** "In Kalender eintragen" asks for the target only when more than one is possible. */
fun ask(targets: List<DDeadlineTarget>): Boolean = targets.size > 1

/**
* The target picked first for a deadline of area [areaId]: my target of that area when it is still possible, else
* the family calendar, else the first one.
*/
fun preselect(rows: List<DDeadlineTargetRow>, areaId: String?, targets: List<DDeadlineTarget>): DDeadlineTarget? {
val mine = rows.firstOrNull { it.areaId == areaId }?.target
return targets.firstOrNull { it.sameAs(mine) } ?: targets.firstOrNull { it.family } ?: targets.firstOrNull()
}

/** A mail calendar as a target, labelled "Outlook · Kalender". */
fun mailTarget(c: DMailCalendar) = DDeadlineTarget(DDeadlineTarget.KIND_MAIL, c.id, mailLabel(c))

fun mailLabel(c: DMailCalendar): String = listOf(c.account.label, c.name).filter { it.isNotBlank() }.distinct().joinToString(" · ")

/**
* The text of a mail target ("Outlook · Kalender"): the calendar as it is now when it is still there, else what
* the server keeps; null for off and the family calendar (the app's own words).
*/
fun mailText(target: DDeadlineTarget?, calendars: DDeadlineCalendars): String? {
if (target?.mail != true) return null
return calendars.mail.firstOrNull { it.id == target.calendarId }?.let(::mailLabel) ?: target.label?.takeIf { it.isNotBlank() }
}

/**
* "Im Kalender · Outlook": the account a deadline's entry went to; empty for a mail calendar without a label, null for
* the family calendar.
*/
fun entryText(entry: DCalendarEntry?): String? {
val t = entry?.target ?: return null
if (!t.mail) return null
return t.label?.substringBefore(" · ")?.trim()?.takeIf { it.isNotEmpty() } ?: ""
}

/** My target of [areaId] now set to [target] (the rows of the settings). */
fun put(rows: List<DDeadlineTargetRow>, areaId: String, target: DDeadlineTarget?): List<DDeadlineTargetRow> =
rows.map { if (it.areaId == areaId) it.copy(target = target) else it }
}
76 changes: 75 additions & 1 deletion app/src/main/java/com/gatecontrol/android/drive/DriveJson.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.gatecontrol.android.drive

import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull

Expand Down Expand Up @@ -731,7 +732,7 @@ internal object DriveJson {
label = o.str("label")?.trim()?.take(200)?.ifEmpty { null },
reminder = r?.let { DReminder(it.str("at")?.take(40), it.int("daysBefore")?.coerceIn(0, 90) ?: 0, it.bool("sent") ?: false) },
node = node(o.obj("node")),
calendar = o.obj("calendar")?.let { c -> DCalendarEntry(c.idString("eventId")?.take(200), c.str("at")?.take(40)) },
calendar = o.obj("calendar")?.let { c -> DCalendarEntry(c.idString("eventId")?.take(200), c.str("at")?.take(40), deadlineTarget(c.obj("target"))) },
nodeId = o.long("nodeId")?.takeIf { it > 0 },
)
}
Expand Down Expand Up @@ -1275,6 +1276,79 @@ internal object DriveJson {
/** `PATCH areas/<id>` → `{ area { deadlinesToCalendar } }`. */
fun areaDeadlinesToCalendar(o: JsonObject): Boolean? = o.obj("area")?.bool("deadlinesToCalendar")

// ─── Drive 1.17: deadlines into a mail calendar (feature `deadlineMailCalendar`) ──

/** `{ kind: 'family' }` | `{ kind: 'mail', calendarId, label }`; anything else (and `null`) is no target. */
fun deadlineTarget(o: JsonObject?): DDeadlineTarget? {
if (o == null) return null
val label = o.str("label")?.trim()?.take(200)?.ifEmpty { null }
return when (o.str("kind")) {
DDeadlineTarget.KIND_FAMILY -> DDeadlineTarget(DDeadlineTarget.KIND_FAMILY, label = label)
DDeadlineTarget.KIND_MAIL -> o.idString("calendarId")?.take(200)?.ifEmpty { null }?.let { DDeadlineTarget(DDeadlineTarget.KIND_MAIL, it, label) }
// the entry of a deadline names only kind and label
?: DDeadlineTarget(DDeadlineTarget.KIND_MAIL, label = label)
else -> null
}
}

/** A target as the server takes it (`null`: off); a mail target needs its calendar. */
fun deadlineTargetJson(t: DDeadlineTarget?): JsonElement = when {
t == null -> JsonNull.INSTANCE
t.mail -> JsonObject().apply {
addProperty("kind", DDeadlineTarget.KIND_MAIL)
addProperty("calendarId", t.calendarId)
t.label?.let { addProperty("label", it.take(200)) }
}
else -> JsonObject().apply { addProperty("kind", DDeadlineTarget.KIND_FAMILY) }
}

/** `GET deadlines/calendars` → `{ family, mail: [{ id, name, account: { id, label, kind }, color }] }`. */
fun deadlineCalendars(o: JsonObject): DDeadlineCalendars? {
val family = o.bool("family") ?: false
val mail = o.arr("mail")?.mapNotNull { e ->
val c = e as? JsonObject ?: return@mapNotNull null
val id = c.idString("id")?.trim()?.take(200)?.ifEmpty { null } ?: return@mapNotNull null
val a = c.obj("account")
val accountId = a?.idString("id")?.take(200) ?: ""
DMailCalendar(
id = id,
name = c.str("name")?.trim()?.take(200)?.ifEmpty { null } ?: id,
account = DMailAccount(accountId, a?.str("label")?.trim()?.take(200).orEmpty(), a?.str("kind")?.take(40)),
color = c.str("color")?.takeIf { COLOR.matches(it) },
)
}?.distinctBy { it.id }?.take(200).orEmpty()
return DDeadlineCalendars(family, mail)
}

/** `GET|PUT deadlines/targets` → `{ targets: [{ areaId, label, target }] }`. */
fun deadlineTargets(o: JsonObject): List<DDeadlineTargetRow>? = o.arr("targets")?.mapNotNull { e ->
val r = e as? JsonObject ?: return@mapNotNull null
val area = areaId(r.str("areaId")) ?: return@mapNotNull null
DDeadlineTargetRow(area, r.str("label")?.take(200) ?: area, deadlineTarget(r.obj("target")))
}?.distinctBy { it.areaId }

/** `PUT deadlines/targets { targets: [{ areaId, target }] }`: my target of area [areaId]. */
fun deadlineTargetsBody(areaId: String, target: DDeadlineTarget?) = JsonObject().apply {
add(
"targets",
JsonArray().apply {
add(
JsonObject().apply {
addProperty("areaId", areaId)
add("target", deadlineTargetJson(target))
},
)
},
)
}

/** `POST deadlines/<id>/calendar { target? }`: without a target (or the family's) as before into the family calendar. */
fun deadlineCalendarBody(target: DDeadlineTarget?) = JsonObject().apply {
if (target?.mail == true) add("target", deadlineTargetJson(target))
}

private val COLOR = Regex("^#[0-9a-fA-F]{6}$")

/** `PUT nodes/<id>/labels { labels, source: 'device' }`: at most [LABELS_MAX] labels of at most [LABEL_MAX] characters. */
fun labelsBody(labels: List<String>) = JsonObject().apply {
add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ data class DMe(
const val FEATURE_PHOTO_PLACES = "photoPlaces"
/** Drive 1.16: people in the photos (faces found on the phones, `people`, `person=`). */
const val FEATURE_PHOTO_PEOPLE = "photoPeople"
/** Drive 1.17: deadlines into a calendar of the mail plugin too (`deadlines/calendars`, `deadlines/targets`). */
const val FEATURE_DEADLINE_MAIL_CALENDAR = "deadlineMailCalendar"
}
}

Expand Down Expand Up @@ -463,8 +465,8 @@ data class DDeadline(
val nodeId: Long? = null,
)

/** A deadline's entry in the family calendar (Drive 1.14). */
data class DCalendarEntry(val eventId: String?, val at: String?)
/** A deadline's entry in the family calendar (Drive 1.14) or, Drive 1.17, in a mail calendar ([target]; null: the family's). */
data class DCalendarEntry(val eventId: String?, val at: String?, val target: DDeadlineTarget? = null)

data class DReminder(val at: String?, val daysBefore: Int, val sent: Boolean)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ internal fun driveText(context: Context, code: String): String = when (code) {
"done_scan_doc" -> context.getString(R.string.drive_ki_done_scan_doc)
"done_calendar" -> context.getString(R.string.drive_ki_done_calendar)
"familie_unavailable" -> context.getString(R.string.drive_ki_familie_unavailable)
"done_calendar_any" -> context.getString(R.string.drive_dl_done_calendar)
"mail_unavailable" -> context.getString(R.string.drive_dl_mail_unavailable)
"rate_limited" -> context.getString(R.string.drive_ki_rate_limited)
"ai_unavailable", "no_ai" -> context.getString(R.string.drive_ki_ai_unavailable)
"no_text" -> context.getString(R.string.drive_insight_no_text)
Expand Down
Loading
Loading