// Package mobile is lectio's engine facade for the dlectio Android app. It is // bound to a native Android library with gomobile: // // gomobile bind -target=android/arm64,android/amd64 -androidapi 26 \ // -javapkg=xyz.labunix.dlectio.engine -tags fullbible \ // -o dlectio-engine.aar github.com/lukaszkasprzak/lectio/mobile // // -javapkg gives the bound Mobile class its xyz.labunix.dlectio.engine.mobile // package (the app imports it from there); the two -target ABIs match the // app's arm64-v8a and x86_64 device/emulator targets; -androidapi 26 matches // the app's minSdk. Omitting any of the three still produces an .aar, just // one the app cannot import or that lacks an ABI it needs. // // The app calls Day() with a plain date/form/version and receives one JSON // document with the day's identity and rendered Mass readings. All liturgical // computation and text resolution happens here, in the exact validated lectio // engine — the app does none of it. Keep every exported signature to types // gomobile can marshal -- strings and ints only -- with JSON as the payload // format for everything structured. A Go int parameter (e.g. Days' count) // surfaces on the Kotlin side as a long, not an Int. package mobile import ( "encoding/json" "strings" "time" "github.com/lukaszkasprzak/lectio/internal/config" "github.com/lukaszkasprzak/lectio/internal/i18n" "github.com/lukaszkasprzak/lectio/internal/readings" "github.com/lukaszkasprzak/lectio/internal/render" ) // reading is one Mass reading, rendered in the requested corpus. type reading struct { Ord int `json:"ord"` Part string `json:"part"` // machine key, e.g. "ewangelium" / "psalm" Heading string `json:"heading"` // human label in the interface language, e.g. "Gospel" Citation string `json:"citation"` Text string `json:"text"` } // dayDoc is the JSON returned by Day. type dayDoc struct { Date string `json:"date"` Form string `json:"form"` Lang string `json:"lang"` Version string `json:"version"` Name string `json:"name"` Season string `json:"season"` Colour string `json:"colour"` Rank string `json:"rank"` RankLabel string `json:"rank_label"` Readings []reading `json:"readings"` Error string `json:"error,omitempty"` } // lectByForm maps the app's form code to lectio's lectionary value. func lectByForm(form string) string { if form == "ef" { return "traditional" } return "new" } // Day computes one day's identity and Mass readings and returns them as JSON. // // date - "YYYY-MM-DD" // form - "of" (Ordinary) or "ef" (1962) // version - corpus: "drb" | "wuj" | "vul" | "grb" // lang - interface language for the celebration name: "en" | "pl" // // It never panics: any engine error is reported in the JSON "error" field so the // JNI boundary always returns a well-formed string. func Day(date, form, version, lang string) string { lect := lectByForm(form) out := dayDoc{Date: date, Form: form, Lang: lang, Version: version} // Day identity (name/season/colour) in the interface language. cfgID := config.Config{UILanguage: lang, Lectionary: lect, All: true} if _, info, err := readings.Load(cfgID, readings.Options{Date: date, All: true}); err == nil { out.Name = info.Name out.Season = info.Season out.Colour = info.Colour out.Rank = info.Rank out.RankLabel = i18n.Get(lang).Rank[info.Rank] } else { out.Error = err.Error() } // Readings, resolved and rendered in the requested corpus. The interface // language localizes the structural labels (Heading) and citation dialect; // the corpus (version) alone decides the scripture text. cfgR := config.Config{UILanguage: lang, Lectionary: lect, ReadingVersion: version, All: true} secs, _, err := readings.Load(cfgR, readings.Options{Date: date, All: true}) if err != nil { if out.Error == "" { out.Error = err.Error() } } else { for i, sec := range secs { // GatherVersion's lang localizes only the label/error wording, never // the verse text (which the corpus alone decides), so the interface // language is correct here. HeadingWithRef gives the same localized // "label (citation)" heading lectio shows in its cli/tui/web. _, blocks := render.GatherVersion(version, sec, lect, lang) out.Readings = append(out.Readings, reading{ Ord: i, Part: sec.PartID, Heading: render.HeadingWithRef(sec, lang), Citation: sec.Citation, Text: strings.Join(blocks, "\n"), }) } } b, err := json.Marshal(out) if err != nil { // Should be impossible with these types; degrade gracefully. return `{"error":"marshal failed"}` } return string(b) } // summaryPart is one section of a day, citation only -- no rendered text. type summaryPart struct { Part string `json:"part"` Citation string `json:"citation"` } // daySummary is one day's identity and its reading citations. It is Day's // cheaper projection: the calendar view needs the day's shape, not its text. type daySummary struct { Date string `json:"date"` Name string `json:"name"` Rank string `json:"rank"` RankLabel string `json:"rank_label"` Colour string `json:"colour"` Parts []summaryPart `json:"parts"` Error string `json:"error,omitempty"` } // Days returns count consecutive days from start as a JSON array, each with its // identity and reading citations but no reading text. It is the calendar view's // source. // // start - "YYYY-MM-DD", taken literally: aligning to a week is the caller's job // count - 1..366; anything else yields "[]" // form - "of" or "ef" // lang - interface language for the name, rank word and citation dialect // // No version parameter: the corpus decides only scripture text, which this never // renders, while the citation's sigla dialect follows lang. // // A day that fails to compute carries its own "error" and does not abort the // rest, so one bad date cannot blank a whole week. Malformed input yields "[]" // rather than an error string, so the caller always parses an array. func Days(start string, count int, form, lang string) string { if count < 1 || count > 366 { return "[]" } d, err := time.Parse("2006-01-02", start) if err != nil { return "[]" } lect := lectByForm(form) ui := i18n.Get(lang) cfg := config.Config{UILanguage: lang, Lectionary: lect, All: true} out := make([]daySummary, 0, count) for i := 0; i < count; i++ { date := d.AddDate(0, 0, i).Format("2006-01-02") row := daySummary{Date: date, Parts: []summaryPart{}} secs, info, err := readings.Load(cfg, readings.Options{Date: date, All: true}) if err != nil { row.Error = err.Error() } else { row.Name = info.Name row.Colour = info.Colour row.Rank = info.Rank row.RankLabel = ui.Rank[info.Rank] for _, sec := range secs { row.Parts = append(row.Parts, summaryPart{Part: sec.PartID, Citation: sec.Citation}) } } out = append(out, row) } b, err := json.Marshal(out) if err != nil { return "[]" } return string(b) } // partLabel is one section's machine ID and its human label. type partLabel struct { Part string `json:"part"` Label string `json:"label"` } // PartLabels returns the form's section IDs and their labels in lang, in display // order, as a JSON array. The app builds its reading filters from this instead // of hardcoding part IDs -- which is how it came to ship seven 1962 checkboxes // the engine never emits. // // An array, not an object: JSON object key order is not guaranteed and the app // renders these in order. func PartLabels(form, lang string) string { ids := readings.PartIDs(lectByForm(form)) ui := i18n.Get(lang) out := make([]partLabel, 0, len(ids)) for _, id := range ids { label := ui.PartLabel[id] if label == "" { label = id } out = append(out, partLabel{Part: id, Label: label}) } b, err := json.Marshal(out) if err != nil { return "[]" } return string(b) } // Ping is a trivial JNI smoke test: it returns "pong" so the app can confirm the // native engine loaded before issuing a real query. func Ping() string { return "pong" }