Secretary

Apps/contacts/changelog
Login

Apps/contacts/changelog

Secretary Contacts (Android) - Changelog

Every released version of the Secretary Contacts app, newest first.

App documentation: Apps/contacts

Status

Status 2026-10-13 (77)

Secretary Contacts 77 (versionCode 77): Ordner und ihre Einträge sind jetzt sichtbar ineinander verschachtelt. Die gemeinsame FolderRow (common/ui/FolderRow.kt) unterschied sich bisher nicht von den Eintragskarten: Ordner-Kopfzeile und Einträge lagen als gleich aussehende Karten in einer flachen LazyColumn, die Zugehörigkeit der Einträge zum Ordner war nicht zu erkennen. Die Kopfzeile ist jetzt eine getönte, abgerundete Zeile mit Ordner-Glyphe, Name, Anzahl und rotierendem Chevron in der Akzentfarbe; jeder Eintrag eines aufgeklappten Ordners ist über die neue Composable FolderChild eingerückt und links mit einer dünnen Führungslinie an die Kopfzeile gebunden. Die Linie wird in den Kartenabstand hinein verlängert und läuft dadurch über die Einträge eines Ordners durchgehend. Das trifft alle vier Listen-Clients (Notizen, Aufgaben, Links, Kontakte). versionCode 76 -> 77.

Tests: :common:compileDebugKotlin, :notes:compileDebugKotlin, :tasks:compileDebugKotlin, :links:compileDebugKotlin, :contacts:compileDebugKotlin grün.

Status 2026-10-09 (74)

Secretary Contacts 74 (versionCode 74): Ordner-Gruppierungen starten eingeklappt, mit gemeinsamer Einstellung Ordner eingeklappt.

(1) Eingeklappt per Vorgabe. Die Listen gruppieren nach Ordnern mit einer Kopfzeile pro Ordner; deren Startzustand ist jetzt eingeklappt, ein Tipp auf die Kopfzeile klappt den Ordner auf/zu (FolderExpansionState/rememberFolderExpansion in common/ui/FolderRow.kt). Eine Liste mit nur einem Ordner zeichnet weder Kopfzeile noch Umschalter.

(2) Gemeinsame Einstellung. Der Startwert kommt aus der gemeinsamen UiPreferences.folders_collapsed (Default true = eingeklappt; SRS/common 1.6.0, FR-C-70..72) und liegt im Einstellungs-Screen in der neuen Sektion Ordner: Checkbox Ordner eingeklappt („Listen starten mit geschlossenen Ordnern"), gerendert von FolderSettingsSection. SecretarySettingsScreen traegt dafuer showFolderSettings – true bei Notizen, Aufgaben, Links und Kontakten, alle übrigen Clients bleiben beim Default false.

Tests: compileDebugKotlin (common + alle Module) und testDebugUnitTest (common + die vier Ordner-Clients) gruen; keine JVM-Verhaltensaenderung, keine neuen Unit-Tests. versionCode 73 -> 74.

Status 2026-10-01 (69)

Secretary Contacts 69 (versionCode 69): die Sync-Aktion ist ohne Konto weg, Passwortfelder zeigen Klartext. Beides sitzt im gemeinsamen Code und trifft damit alle acht Clients.

(1) Keine Sync-Aktion ohne Konto. AppHeader (Kit.kt) blendet die Sync-Aktion bereits aus, wenn onSync null ist - das war die vorhandene Konvention, nur wurde sie an keiner Stelle benutzt. OfflineBanner (StatusBanners.kt) verlangte dagegen einen nicht-nullbaren onSync und zeichnete den Refresh-Button bedingungslos; der Parameter ist jetzt (() -> Unit)?, und ohne Konto fehlt der Knopf (die Meldung "offline" bleibt stehen). Alle Aufrufstellen reichen den Callback nur noch durch, wenn wirklich ein Konto existiert: loggedIn der jeweiligen ViewModel, im Kalender der collected isLoggedIn-StateFlow. Das Journal ist der einzige Client, bei dem das sichtbar war - es hat keine Login-Sperre und laeuft im lokalen Modus (localMode = !repo.isConfigured()) ohne Konto weiter, zeigte den Sync-Knopf aber trotzdem. Beim Secrets-Tresor war der Knopf vorhanden und nur deaktiviert (enabled = state.connected); er wird jetzt gar nicht erst gezeichnet, der Spinner waehrend state.syncing bleibt. Am Launcher aendert sich nichts: die Server-Sync-Sektion im Settings-Screen war schon an account != null gehaengt.

(2) Passwortfelder zeigen Klartext. Alle Eingabefelder fuer Passwoerter nehmen VisualTransformation.None; die Tastatur bleibt KeyboardType.Password. Betroffen sind der gemeinsame Login-Screen (SecretaryLoginScreen), die Journal-Passwortdialoge (PasswordDialogs: Setzen, Aendern, Abfragen) und das Entschperren (UnlockScreen) sowie im Tresor die Felder fuer Recovery-Passphrase, Entschperr-Passphrase, KDBX-Import und KDBX-Export (SecretsApp) und das Passwortfeld im Eintrags-Editor (Browse). Anlass war die Darstellung, nicht die Sicherheit: die Maske besteht aus U+25CF (●), und die gebundelte Typewriter-Schrift Special Elite deckt dieses Zeichen nicht ab - die Felder zeigten unlesbare Ersatzglyphen. In den Tresor-Dialogen ist der Sichtbarkeits-Umschalter damit ueberfluessig und mit ihm die Felder passwordVisible/visible, der Eye-Button bzw. der Textbutton Anzeigen/Verbergen und die Visibility/VisibilityOff-Imports weg. Der Reveal-Pfad fuer gespeicherte Secrets in Browse (FieldRow) bleibt unberuehrt: das ist eine Anzeige, keine Eingabe.

Tests: keine neuen, kein JVM-Verhalten geaendert. 662 Tests gruen (Kalender 55, common 100, Kontakte 44, Launcher 12, Secrets 405, Tasks 46), verifiziert ueber gradle test; alle neun Module kompilieren (compileDebugKotlin, offline), und die Neuuebersetzung meldet in keiner der geaenderten Dateien eine Warnung. versionCode 68 -> 69.

Status 2026-09-30 (68)

Secretary Contacts 68 (versionCode 68): keine Code-Aenderung - der Quelltext ist derselbe wie bei 67. Veraendert wurde in diesem Zug nur der gemeinsame SecretarySettingsScreen in common (SRS common 1.3.0), den auch Contacts traegt: neue Versionszeile am Ende (aus der PackageInfo des eigenen Pakets) und die Namensraum-Karte raus, die hier ohnehin nie benutzt wurde - die Karte Sammlungspräfix bleibt, weil der Wert dort konfigurierbar ist. Am Contacts-Code aendert sich keine Zeile. Die Nummer geht trotzdem hoch, weil ein Release alle acht Clients und beide Serverpakete zusammen ausliefert: das Buildsystem ueberspringt einen Lauf, dessen Zieldateinamen alle bereits im Archiv liegen, und ein gleichnamiges Paket mit gleichem Inhalt waere auf dem Geraet nicht von 67 zu unterscheiden - bei einer APK zusaetzlich ein versionCode, den ein Geraet nicht neu installiert. versionCode 67 -> 68.

Status 2026-09-30 (67)

Secretary Contacts 67 (versionCode 67): Kontaktfotos waren nie kaputt – sie wurden auf dem Weg zum Spiegel abgeschnitten. Nachdem 66 den Spiegel endlich schreiben laesst, erschienen die Kontakte zwar mit Namen, aber ohne Bild. Ursache ist nicht der Provider und nicht der Server, sondern eine Doppel-Faltung: Base64.DEFAULT (ContactSystemSyncAdapter, Upload-Richtung) bricht das Foto-Base64 alle 76 Zeichen um, und VCard.fold() faltet diesen String ein zweites Mal – diesmal mitten durch die eingebauten Zeilenumbrueche. Ergebnis auf der Platte sind abwechselnd markierte und unmarkierte Fortsetzungszeilen. Ein RFC-konformer Parser (so VCard.unfold, so DAVx5, so jede andere App) haelt eine unmarkierte Zeile fuer eine neue Property und bricht die Property nach der ersten physischen Zeile ab: 76 Zeichen, also genau der JPEG-Kopf. Genau diese 76 Zeichen standen in allen 8 Karten mit PHOTO auf dem Server – und genau diese 76 Zeichen landeten als "Foto" im System-Provider.

Belegt, ohne zu raten: 1d656402….vcf auf dem Server ist 10 674 Byte gross und unveraendert lesbar. Nach dem Entfalten ergibt sich ein vollstaendiges Base64 von 9 988 Zeichen, das 7 491 Byte dekodiert – ein 96x96-JPEG mit ffd8 am Anfang und ffd9 am Ende. Die Bilddaten waren also nie verloren, nur die Zeilenfuehrung war zerstoert. Ein kleiner Reproducer bestaetigt die Mechanik byteweise: fold("PHOTO;ENCODING=b;TYPE=JPEG:" + base64.chunked(76)) erzeugt dieselbe Datei, 131 von 268 Fortsetzungszeilen ohne fuehrendes Leerzeichen.

Drei Aenderungen:

  1. Die Ursache beseitigt. ContactSystemSyncAdapter kodiert das vom Provider gelesene Foto jetzt mit Base64.NO_WRAP statt Base64.DEFAULT (Zeile 585ff). PhotoCodec.encode nutzte bereits Base64.getEncoder(), also einwandfrei; nur der Upload-Pfad aus dem Provider war befallen.
  2. fold() ist jetzt belastbar. VCard.fold zerlegt die Zeile an ihren eigenen Zeilenumbruechen und an der 75/74-Oktet-Grenze und stellt jeder Teilstuecke das fuehrende Leerzeichen vor. Auch ein bereits umgebrochenes Base64 wird damit RFC-konform gefaltet statt mitten zerschnitten. Damit kann ein umgebrochener Wert das Ergebnis nicht mehr zerstoeren.
  3. Der Parser repariert, statt zu truncaten. VCard.unfold fuegt unmarkierte Base64-Fortsetzungszeilen einer PHOTO;ENCODING=b-Property wieder zusammen – nur dort, nur wenn die Zeile kein : enthaelt (also keine Property sein kann) und nur solange sie aus Base64-Zeichen besteht. Eine echte Property-Zeile (TEL;TYPE=CELL:…) wird nie verschluckt; ein PHOTO;VALUE=URI: bleibt unberuehrt. Damit werden die bereits auf dem Server liegenden Karten beim naechsten Abruf automatisch lesbar, ohne einen Schreibzugriff.

Zusaetzlich wurden die 8 betroffenen vCards auf dem Server (~/.local/share/secretary-server/addressbooks/) repariert: unmarkierte Fortsetzungszeilen wieder angehaengt und die PHOTO-Zeile nach RFC neu gefaltet, jede Datei gegen ffd8/ffd9 geprueft (3 550 bis 7 491 Byte). Sicherung davor: ~/vcf-backup-photos-20260930-1426.tar.gz. Ein zweiter Durchlauf findet 0 betroffene Dateien.

9 neue Tests in PhotoRepairTest (common): Reparatur der Doppel-Faltung auf Bytes, fold() erzeugt nie eine unmarkierte Fortsetzungszeile und haelt 75-Oktet-Grenzen ein, verlustfreier Round-Trip, CRLF, eine echte Property und eine zweite NOTE hinter dem Foto werden nicht verschluckt, PHOTO;VALUE=URI: bleibt unberuehrt. 662 Tests gruen.

Nicht geaendert: Contact/ContactModels, PhotoCodec, ContactsCodec (dort wird photoB64 bereits als JSON-String persistiert), die Provider-Seite. Geaendert: android/common/.../VCard.kt, android/contacts/.../ContactSystemSyncAdapter.kt, android/common/src/test/.../PhotoRepairTest.kt (neu), build.gradle.kts, CONTEXT.md. Am Server nur die 8 vCard-Dateien, kein Code.

versionCode 66 -> 67.

Status 2026-09-30 (66)

Secretary Contacts 66 (versionCode 66): der Spiegel hat auf diesem Gerät nie eine Zeile geschrieben – WRITE_CONTACTS war nie freigegeben. Damit ist zum ersten Mal die Ursache benannt, an der die Stände 59 und 60 mit ihren E.164- und Leerbuch-Korrekturen gescheitert sind: sie haben alle einen Codepfad korrigiert, der nie durchlaufen wurde.

Die Berechtigung ist im Manifest deklariert, aber WRITE_CONTACTS ist seit API 23 eine Laufzeitberechtigung – die Deklaration allein gewährt nichts, und READ_CONTACTS (das die App für die Messenger-Kontakte anfragt und auch erhalten hat) hat offenbar den Eindruck erweckt, der Kontakt-Provider sei zugegriffen. Auf dem Geraet: READ_CONTACTS: granted=true, WRITE_CONTACTS: granted=false. Jeder applyBatch des Spiegels endete mit

SecurityException: Permission Denial: writing com.android.providers.contacts.ContactsProvider2
uri content://com.android.contacts/raw_contacts from pid=…, uid=…
requires android.permission.WRITE_CONTACTS, or grantUriPermission()

und der Fehler wurde pro Kontakt gefangen (Log.w(TAG, "insert … failed", e), numIoExceptions). Der Lauf meldete danach 0 inserted / 0 updated und damit Erfolg. Im Provider standen null RawContacts unter org.beowulf.secretary.addressbook – bei 69 Kontakten im Cache. Fuer Wähler und WhatsApp sieht das ununterscheidbar aus wie "der Server hat nichts geliefert" oder "die Nummern sind nicht normalisiert", und genau diese beiden Erklaerungen wurden in 59 (address book unknown, skipping) und 60 (Phone.NORMALIZED_NUMBER aus E.164) verfolgt.

Belege auf dem Geraet, in dieser Reihenfolge: dumpsys package zeigt die Berechtigung als granted=false; der Sync-Adapter protokollierte die SecurityException; ein content query auf raw_contacts mit account_type='org.beowulf.secretary.addressbook' lieferte No result found. Nach adb shell pm grant org.beowulf.secretary.contacts android.permission.WRITE_CONTACTS und einem Kaltstart schrieb unveraendertes 65 69 inserted / 0 updated (Kontakte) und 0 inserted / 4 updated (Aerzte), und PhoneLookup loeste sofort auf:

content://com.android.contacts/phone_lookup/+491727766616 -> Thomas Schneider, number=0172 7766616
content://com.android.contacts/phone_lookup/01727766616 -> Thomas Schneider

Damit ist zugleich die E.164-Theorie aus 60 widerlegt: der Provider bildet die E.164-Form selbst (AOSP DataRowHandlerForPhoneNumber.fillNormalizedNumber ruft PhoneNumberUtils.formatNumberToE164(NUMBER, CountryMonitor), und CountryMonitor faellt auf Locale.getDefault().getCountry() zurueck), der Probe zaehlt provider normalized=7/7 bei phone_lookup hit=7 miss=0, und das gilt fuer nationale Nummern mit trunk-Null genauso wie fuer +49 2402 28628. Ein zusaetzlicher, selbst gerechneter NORMALIZED_NUMBER koennte dem Provider nichts geben, was er nicht schon hat – im Gegenteil, er muesste dafuer eine Laenderkennungstabelle pflegen und die Anzeige verlustbehaftend veraendern (+49 30 1234567 -> +49301234567). Ein entsprechender Versuch wurde deshalb wieder verworfen; PhoneNumbers ist unveraendert und der berechtigte Fehlerfall (+49 (030) 12 34-567, trunk-Null hinter der Laenderkennung) bleibt beim Provider.

Der Fix besteht aus vier Teilen:

  1. Einmal prüfen statt N-mal scheitern. ContactSystemSyncAdapter.hasWritePermission (Companion) am Anfang von syncLocked: ohne die Berechtigung eine Zeile Log mit Ursache und Abbruch, numAuthExceptions += 1, kein Serverabruf und kein Insert-Sturm. Das catch (e: Exception) pro Kontakt bleibt fuer echte Transportfehler stehen, kann eine fehlende Berechtigung aber nicht mehr als "0 inserted" tarnen.
  2. Fragen, wo gefragt werden kann. MainActivity.onCreate startet den RequestPermission-Launcher fuer WRITE_CONTACTS, wenn sie fehlt – dort laeuft kein Sync, also ist das der richtige Ort. Hoechstens einmal pro Installation (permission_state/asked_write_contacts): nach zwei Ablehnungen zeigt Android keinen Dialog mehr, und ein weiterer Start wuerde stillschweigend "nein" liefern.
  3. Nach der Freigabe sofort spiegeln. ContactsViewModel.onSystemWritePermissionChanged ruft ContactSystemScheduler.rearm auf; da nie etwas geschrieben wurde, fuellt der erste Lauf mit der Berechtigung den Provider in einem Zug. Bewusst rearm und nicht ensureSyncing: die Antwort aus dem Systemdialog kommt zurueck, bevor die CardDAV-Sammlungen geladen sind, und reconcile entfernt jedes Konto, dessen Buch-id nicht in der uebergebenen Liste steht - eine leere Liste wuerde also saemtliche Adressbuch-Konten loeschen und damit den ganzen Spiegel. Genau das beschreibt der Kommentar an rearm, und der Buchpfad reconciliert beim Laden von sich aus.
  4. Der Zustand wird sichtbar. systemWritePermissionGranted in der Settings-Sektion "Systemkontakte": roter Hinweis plus Knopf "Zugriff auf Systemkontakte erlauben", und der Sync-Knopf meldet "Keine Schreibberechtigung – bitte oben erlauben." statt "Synchronisierung angefordert." (Damit ist der Knopf der auch nach einer Ablehnung der einzige Weg zum Grant, weil der Dialog dann nicht mehr kommt.)

Dazu system/SystemContactsProbe (aus 65, hier erstmals wirksam geworden): es protokolliert je Konto raw=, Namenszeilen, aggregated=, phone rows=, provider normalized= und phone_lookup hit=/miss=. Genau seine Zeile raw=0 sampled=0 | … | phone_lookup hit=0 miss=0 macht den Zustand auf einen Blick sichtbar; der Stacktrace der SecurityException war zwar auch da, wurde aber als einer unter vielen gelesen. Am Instrument ist nichts geaendert.

Geaendert: android/contacts/build.gradle.kts, MainActivity.kt, system/ContactSystemSyncAdapter.kt, ui/SettingsScreen.kt, vm/ContactsViewModel.kt, CONTEXT.md. Am Server aendert sich nichts. Die Berechtigungsliste in CONTEXT.md behauptete bisher WRITE_CONTACTS (signature/install); das ist seit API 23 falsch und wird hier korrigiert – die Zeile hat den Fehler mit verschleiert. Nachgewiesen auf dem Geraet mit dumpsys package, content query und dem Sync-Log; die Modultests (44) sind gruen, die Berechtigungslogik laeuft in keinem Test, weil sie Context und ein Geraet braucht. versionCode 65 -> 66.

Status 2026-09-30 (64)

Secretary Contacts 64 (versionCode 64): keine Code-Aenderung - der Quelltext ist derselbe wie bei 63. Veraendert wurde in diesem Zug nur der Launcher (Korrektur am Konfigurations-Sync, dort 460). Die Nummer geht trotzdem hoch, weil ein Release alle acht Clients und beide Serverpakete zusammen ausliefert: das Buildsystem ueberspringt einen Lauf, dessen Zieldateinamen alle bereits im Archiv liegen, und ein gleichnamiges Paket mit gleichem Inhalt waere auf dem Geraet nicht von 63 zu unterscheiden - bei einer APK zusaetzlich ein versionCode, den ein Geraet nicht neu installiert. Server und check-links gehen mit auf 0.24.0. versionCode 63 -> 64.

Status 2026-09-30 (63)

Secretary Contacts 63 (versionCode 63): erster Stand, der auf trunk liegt, und damit der erste, den ein Release-Build überhaupt sehen kann. Gegenüber 62 und 61 gibt es keine Code-Änderung; der Boot-Receiver aus 61 (BOOT_COMPLETED, MY_PACKAGE_REPLACED, QUICKBOOT_POWERON) und die Refresh-Semantik aus 60, die nur einen erfolgreichen Abruf ein leeres Buch melden lässt, sind unverändert enthalten.

Der Weg dahin war nötig, weil das Buildsystem im Release-Modus nicht das Arbeitsverzeichnis nimmt, sondern http://tremere:8084/secretary klont und darauf fossil open macht – laut fossil help open den jüngsten Commit auf dem Hauptbranch. trunk zeigte bis zum Zusammenführen auf ce87fc0c vom 29.09. (Kontakte 58), während 62 und 0.22.0 als Nachfahren darunter lagen und von keinem Branch-Zeiger erreicht waren. Ein Release-Lauf hat daraufhin ce87fc0c gebaut und Kalender 96, Kontakte 58 und Launcher 457 veröffentlicht, also ältere Stände als die über --debug gebauten. 61, 62 und Server 0.21.0/0.22.0 sind nie erschienen.

versionCode 62 -> 63.

Status 2026-09-30 (62)

Secretary Contacts 62 (versionCode 62): Gegenüber 61 gibt es keine Code-Änderung, der Stand ist bisher nicht veröffentlicht. Der Wunsch war, 61 und 62 über den Release-Pfad zu bauen, weil --debug das Arbeitsverzeichnis statt des committeten Standes als Quelle nimmt und seine Artefakte trotzdem in den Artefakt-Pool ablegt. Der Release-Lauf ist aber gescheitert: das Buildsystem im Release-Modus klont http://tremere:8084/secretary und macht fossil open darauf, und der Branch, den der Build thereby öffnet, steht auf ce87fc0c vom 29.09. (Kontakte 58). Der Release-Lauf hat deshalb Kalender 96, Kontakte 58 und Launcher 457 gebaut und veröffentlicht, also ältere Stände als 61. Ein Release-Build ist erst wieder korrekt, wenn der Branch, den der Build öffnet, den committeten Stand enthält.

Damit ändert sich am Verhalten gegenüber 61 nichts: Der Boot-Receiver aus 61 (BOOT_COMPLETED, MY_PACKAGE_REPLACED, QUICKBOOT_POWERON) ist unverändert enthalten, ebenso die Refresh-Semantik aus 60, die nur einen erfolgreichen Abruf ein leeres Buch melden lässt. Die Versionsnummer ist deshalb der einzige Unterschied, damit das Gerät die Version als Update installiert und nicht stillschweigend eine APK mit derselben versionCode liegen lässt. versionCode 61 -> 62.

Status 2026-09-30 (61)

Secretary Contacts 61 (versionCode 61): Der Abgleich in den System-Provider passiert auch ohne App-Betrieb – nach Neustart und nach einem Update. Bisher gab es nur zwei Wege, die Kontakte in den Provider zu bekommen: der Knopf „Jetzt in Systemkontakte synchronisieren“ (der ensureSyncing aufruft und den Spiegel selbst in einem Thread des App-Prozesses laufen lässt – deshalb wirkt er immer) und der 6-h-periodische Sync, den enablePeriodicSync beim SyncManager anmeldet. Der periodische Sync ist der einzige Weg, der ohne laufende App funktioniert, wurde aber praktisch nie ausgeführt: der System-Kontakte-Provider cached die Sync-Adapter- und Account-Metadaten über App-Updates hinweg (in Status 37 bereits vermerkt: „Nach Installation muss die App force-gestoppt werden“), sodass direkt nach einem Update die Accounts weiterhin gegen den alten Adapter geplant wurden – oder gar nicht. In diesem Moment läuft in der App nichts, also blieb der Spiegel leer, bis der Nutzer die App öffnet und den Knopf drückt. Genau das war gemeldet worden: „nie von allein, nur auf Anforderung“.

Neu ist system/ContactSystemBootReceiver (Manifest-Receiver für BOOT_COMPLETED, MY_PACKAGE_REPLACED und QUICKBOOT_POWERON, Berechtigung RECEIVE_BOOT_COMPLETED). Er ruft ContactSystemScheduler.rearm(), das periodische Sync für die bereits vorhandenen Accounts neu anmeldet, den Spiegel im Hintergrund laufen lässt und zusätzlich einen System-Sync anfordert. ensureSyncing besteht jetzt aus reconcile() + rearm() – der Normalfall im App-Betrieb ist unverändert.

Wichtig: rearm() ruft bewusst nicht reconcile(). Zum Bootzeitpunkt sind die CardDAV-Collections nicht geladen, und reconcile() entfernt jeden Account, dessen Buch-ID nicht in der übergebenen Liste steht – eine leere Liste würde also beim ersten Neustart alle Adressbuch-Accounts löschen und damit den ganzen Spiegel. Genau das ist im KDoc von rearm() festgehalten.

Der Spiegel läuft aus dem Offline-Cache, ein Neustart ohne Netz kommt also nicht ins Leere (dank der Refresh-Semantik aus 60: nur SYNCED darf ein leeres Buch melden). Die schwere Arbeit passiert im Thread in rearm(), deshalb kehrt der Receiver sofort zurück und läuft nicht in die 10-s-Grenze von Broadcasts. addPeriodicSync wird bei jedem rearm erneut aufgerufen; das ist absichtlich idempotent, addPeriodicSync ersetzt den bestehenden Eintrag für dasselbe Konto/Authority-Paar.

Nicht getestet: ohne Robolectric (die Unit-Tests sind reine JVM-Tests) lässt sich der Receiver nicht automatisiert prüfen; verifiziert sind Kompilierung und die bestehenden Tests. Ein Gerätetest (Neustart bzw. Update, dann adb shell dumpsys account) steht noch aus. versionCode 60 -> 61.

Status 2026-09-30 (60)

Secretary Contacts 60 (versionCode 60): WhatsApp löst Namen wieder auf – der Mirror löscht das Adressbuch nicht mehr, wenn der Server nicht erreichbar ist, und der phone_lookup-Schlüssel wird aus E.164 gebildet. Der Stand 59 holte ein nie geöffnetes Buch vor dem Spiegeln vom Server, behandelte aber einen gescheiterten Abruf wie einen leeren Cache – und der Mirror löscht jede RawContact, deren uid nicht im Cache steht. Ein einziger fehlgeschlagener Abruf im periodischen Sync (6 h, offline) löschte damit das komplette Buch aus dem System-Provider; WhatsApp und der Wähler zeigten danach nur noch Rufnummern. Drei weitere Ursachen kamen dazu:

  1. Kein Buch im Provider löschen, nur weil nichts geholt werden konnte. refreshFromServer gibt jetzt Refresh.SYNCED / FAILED / UNKNOWN_BOOK zurück. Nur SYNCED darf ein leeres Buch melden; bei FAILED/UNKNOWN_BOOK und leerem Cache lässt syncLocked den Provider unangetastet und loggt es. Der Upload (DIRTY/DELETED) läuft weiterhin, ist aber vom Mirror entkoppelt.
  2. Unbekanntes Buch wird beim Server nachgeschlagen, nicht übersprungen. snap.collections schreibt nur die App beim Laden der Adressbücher; ein kopfloser Lauf (Sync-Manager, periodisch, ohne UI-Zugriff) hatte dort nichts und kehrte mit "address book unknown, skipping" zurück – auf einer frischen Installation landete so gar nichts im Provider. refreshFromServer holt die Collection-Liste per PROPFIND, übernimmt sie in den Cache (die gerätelokalen Pseudo-Bücher „Lokal"/„Messenger" bleiben dabei erhalten) und spiegelt erst danach.
  3. UID-Fallback wieder Namens+Telefon+E-Mail (Regression). VCard.parse bildete die Fallback-UID nur aus FN („unknown" ohne FN), obwohl Status 14 seit 2026-08-28 ausdrücklich Name plus Telefonnummern plus E-Mails hasht. Der Mirror schlüsselt über die uid: gleichnamige Karten ohne UID teilten sich eine uid, wurden im selben Lauf doppelt eingefügt (die existing-Map wird nach dem Einfügen nicht neu gelesen) und im nächsten Lauf auf eine Zeile kollabiert – der Verlierer war endgültig weg. Der Hash umfasst jetzt wieder alle drei Felder (deterministisch, also stabil über Parse/Persist/Round-Trip), und dedupeByUid lässt Kollisionen nicht mehr mehrfach schreiben.
  4. Phone.NORMALIZED_NUMBER aus E.164. WhatsApp löst Namen ausschließlich über ContactsContract.PhoneLookup auf, und die phone_lookup-Tabelle entsteht aus Data.NUMBER/NORMALIZED_NUMBER. Bisher ging der rohe vCard-TEL ungefiltert in NUMBER – Tel:-Präfix, ;ext=3 oder die 00-Präfixform liefern dort keinen brauchbaren Schlüssel, der Kontakt liegt dann im System, WhatsApp zeigt aber nur die Nummer. Neu ist system/PhoneNumbers (reines Kotlin, ohne android.telephony und ohne Region): es entfernt Tel:/Fax: und <>, trennt die Nebenstelle ab (;ext=, ;3, ext. 3, x89, #3), wandelt 00 in + und schreibt die E.164-Nummer zusätzlich als NORMALIZED_NUMBER. Bewusst konservativ: nur reine Ziffernwerte mit +/00 werden normalisiert, weil eine trunk-präfix-behaftete Form wie +49 (030) 12 34-567 sich ohne Länderdaten nicht korrekt in E.164 überführen lässt – dort entscheidet der Provider selbst mit der Geräte-Region. Nationale Nummern bleiben national. 14 neue Tests (PhoneNumbersTest) + 5 (VCardTest Fallback-UID). Die geänderten UIDs erzeugen einmaliges Umschreiben der betroffenen RawContacts (alt → neu), danach ist der Zustand stabil.

Status 2026-09-29 (59)

Secretary Contacts 59 (versionCode 59): Alle Collections landen im System-Provider (WhatsApp löst wieder Namen auf). Ursache: der Offline-Cache (secretary_contacts_cache.json) enthielt nur die Adressbücher, die der Nutzer in der App tatsächlich geöffnet hatte – loadContactsInternal wurde nur für das ausgewählte Buch aufgerufen. Der Mirror (ContactSystemSyncAdapter.sync) las dagegen store.snapshot().itemsByCollection und spiegelte folglich alle nie geöffneten CardDAV-Collections als leer in den System-Provider. Ergebnis: in WhatsApp/Dialer standen bei vielen Kontakten nur die Rufnummern statt der Namen. Fix: lädt der SyncAdapter ein Buch, für das der Cache noch nichts enthält, die Collection jetzt selbst über den OfflineSyncEngine (derselbe Pfad wie die App, inkl. Pending-Queue-Semantik) vom Server, bevor er spiegelt; ein Transportfehler lässt den Cache unangetastet und der Mirror verarbeitet weiterhin den vorhandenen Stand. Damit spiegelt der Adapter jede Collection, unabhängig davon, ob sie je in der App geöffnet wurde – auch beim periodischen Sync (6 h) und ohne jeden UI-Zugriff. Zusätzlich schützt ein pro Konto gesetztes ReentrantLock (tryLock) davor, dass der direkte Sync aus ContactSystemScheduler.ensureSyncing und ein paralleler Lauf des Sync-Managers dasselbe Konto gleichzeitig bearbeiten (Doppel-Inserts und veraltende Cache-Schreibvorgänge). Das Pseudoadressbuch „Lokal" bleibt wie dokumentiert geräte-lokal (kein Konto, kein Mirror); „Messenger" stammt aus dem System-Provider und wird weiterhin übersprungen. versionCode 58 -> 59.

Status 2026-09-20 (52)

Secretary Contacts 52 (versionCode 52): Karteninhalt verteilt statt großem Mittelloch. Die rechte Kartenseite nutzt Arrangement.SpaceEvenly statt des weight(1f)-Abstandshalters vor der Adresse: Der freie Platz wird gleichmäßig zwischen allen Blöcken (Name/Rolle, Telefon/E-Mail-Zeilen, Adresse) verteilt statt als ein großer leerer Streifen zwischen den Nummern und der unten angehefteten Adresse zu versickern. Adresse bleibt weiterhin der unterste Block. versionCode 51 -> 52.

Status 2026-09-20 (51)

Secretary Contacts 51 (versionCode 51): **Karussell wieder da (flüssig

Status 2026-09-20 (50)

Secretary Contacts 50 (versionCode 50): Karte im vCard-Stil + flüssiges kinetisches Scrollen + Namen brechen um. (1) Das Karussell (VerticalPager mit Cover-Flow-Effekten + Snap-Ruckeln) ist durch eine einfache Endlos-LazyColumn ersetzt: der Zähler startet weit vorne (initialFirstVisibleItemIndex = size * 1000), der echte Kontakt ergibt sich über Math.floorMod(index, size) (läuft in beide Richtungen endlos weiter); das native LazyColumn-Fling scrollt dadurch kinetisch und flüssig, ohne Pager-Snap-Animationen, Z-Index/GraphicsLayer-Aufwand. (2) Die rechte Kartenseite sieht jetzt wie eine echte vCard aus: Name in Special Elite wird nie gekürzt, sondern bricht bei Bedarf um (maxLines entfernt), darunter die Rolle (Firma · Position), dann die Telefon-/E-Mail-Zeilen mit Typ-Token rechts (CELL, HOME, …) und ganz unten die mehrzeilige Adresse; das linke Drittel (Aktionen oben, rundes Foto mittig) ist durch einen dünnen vertikalen Trenner abgesetzt. Karten haben keine feste Höhe mehr (heightIn(min = 196.dp)) und wachsen bei langen Namen mit. versionCode 49 -> 50.

Status 2026-09-20 (49)

Secretary Contacts 49 (versionCode 49): Kontaktkarte neu strukturiert + kinetisches Endlos-Karussell + CATEGORIES-Roundtrip-Fix. (1) Die Karussell-Karte ist wieder am früheren Layout orientiert: links sitzen oben die Aktions-Icons (Anruf und Favoriten-Schalter, zzgl. des Messenger-Merge-Buttons) und darunter mittig wieder das runde Kontaktfoto (ContactAvatar, 80 dp Kreis) im linken Drittel; rechts stehen der Name in der Typewriter-Schrift Special Elite und größer (headlineSmall), darunter die Telefonnummern (max. 2, mit Anruf-Icon), die E-Mail und ganz unten die Adresse. Die in 46 eingeführte ContactPhoto-Composable (vollflächiges Foto im linken Drittel) ist entfernt; die Fotos werden wieder als Kreis gerendert. (2) Scrolling: der VerticalPager läuft jetzt unendlich weiter (große initialPage + Math.floorMod-Index-Mapping statt page !in indices) und fliegt kinetisch über bis zu vier Karten pro Wurf (PagerDefaults.flingBehavior mit PagerSnapDistance.atMost(4), snapPositionalThreshold = 0.8) statt strikt eine Karte pro Swipe. (3) CATEGORIES-Fix (secretary-common-android): VCard.serialize escapete Kategorie-"Werte" doppelt (line() escapete das bereits mit escape(it) gefügte Ergebnis → die Komma-Trennzeichen wurden mit-escaped und der Parser verschmolz die Liste zu EINEM Wert [Familie,Arbeit]). Jetzt wird die CATEGORIES-Zeile direkt gefaltet (fold("CATEGORIES:…joinToString(","){escape(it)}")): Kommas in einem Kategorie-Wert bleiben \,, die Trennzeichen bleiben unescaped; der Round-Trip-Test des App-Moduls wurde auf das korrigierte Verhalten angepasst. Alle 25 App- + gemeinsamen Tests grün. versionCode 48 -> 49.

Status 2026-09-20 (46)

Secretary Contacts 46 (versionCode 46): Karussell-Fixes: flüssiges Scrollen + Fotos sichtbar. (1) Das vertikale Karussell aus 45 ruckelte: Die Seitenabstandswerte wurden in der Komposition gelesen (dist für zIndex), wodurch bei jedem Scroll-Frame alle komponierten Seiten inkl. Karteninhalt neu komponierten. Jetzt liest allein der graphicsLayer (Zeichnungsphase) currentPage/currentPageOffsetFraction; der zIndex hängt nur noch am (nur beim Seitenwechsel wechselnden) currentPage und beyondViewportPageCount ist auf 2 reduziert (genau die 5 sichtbaren Karten). Damit werden Scroll-Frames ohne Recomposition gerendert. (2) Das Foto hat die neue ContactPhoto-Composable übernommen: statt eines kleinen Kreis-Avatars auf farbigem Grund füllt das Kontaktfoto jetzt vollflächig das linke Drittel der Karte (zugeschnitten, abgerundete linke Ecken); ohne Foto weiterhin Initialen auf der deterministischen Farbe. versionCode 45 -> 46.

Status 2026-09-20 (45)

Secretary Contacts 45 (versionCode 45): Kontaktliste als vertikales Karussell. Die Liste (LazyColumn → VerticalPager) zeigt bis zu fünf Kontakt-Karten übereinander: die mittlere liegt vollständig im Vordergrund, die übrigen schauen oben/unten mit einem Streifen dahinter hervor (Peek-Faktor 0,35) und gleiten beim Scrollen mit Deckkraft-/Skalenabfall sowie leichter X-Rotation aus. Jede Karte hat links das Kontaktfoto auf farbigem Grund (linkes Drittel, auslaufend bis an die Kartenkante) und rechts Name, Metadaten, Aktionen (Anruf, Favorit) sowie das Langdruck- Kontextmenü (Bearbeiten/Teilen/Kopieren/Verschieben/Löschen wie bisher). Verhalten unverändert: Suche, Kategorie-Filter, Adressbuch-Wechsel und Messenger-Merge ersetzen die Liste (Scroll-Position wird dabei zurückgesetzt). versionCode 44 -> 45.

Status 2026-09-16 (44)

Secretary Contacts 44 (versionCode 44): Offline-Sync-Engine (secretary-common-android v0.30.0). loadContactsInternal (CardDAV-Zweig) nutzt jetzt die gemeinsame OfflineSyncEngine (CrudItemRemote + GOfflineStore

Status 2026-09-13 (43)

Secretary Contacts 43 (versionCode 43): Kategorie-Parsing gehärtet (secretary-common-android v0.26.0). Der gemeinsame VCard-Parser schreibt CATEGORIES jetzt escaped (Komma in einem Kategorie-Wert als \,) und liest die Liste nur noch an unescaped-gespeicherten Kommas; jedes Token wird anschließend dekodiert. Vorher wurde bei jedem Komma geschnitten – eine Kategorie mit Komma im Namen (oder RFC-escapte \,-Werte aus importierten vCard-Dateien) wurde in mehrere Kategorien zerrissen. versionCode 42 -> 43.

Status 2026-09-12 (42)

Secretary Contacts 42 (versionCode 42): Globale Kontakt-Suche + Launcher-Verknüpfungen. searchable.xml + search/SearchActivity (android.intent.action.SEARCH, in der App- und System-Suche über android.app.default_searchable erreichbar) durchsuchen den Offline-Cache (Name, Organisation, Telefon, E-Mail); ein Tipp auf einen Treffer öffnet den Kontakt über ACTION_EDIT in MainActivity. Suchvorschläge liefert der eigene ContactsSearchRecentSuggestionsProvider (Authority org.beowulf.secretary.contacts.search_suggestions; MatrixCursor-Provider, da der SearchRecentSuggestions-Wrapper im getrimmten SDK-Jar fehlt – wie beim Kalender). MainActivity trägt die Launcher-Verknüpfungen @xml/shortcuts („Neuer Kontakt" → ACTION_INSERT auf vnd.android.cursor.dir/contact, „Suchen" → SEARCH). versionCode 41 -> 42.

Status 2026-09-12 (41)

Secretary Contacts 41 (versionCode 41): AOSP-Oberfläche: QuickContact, SHOW_OR_CREATE, PICK/GET_CONTENT, EDIt/VIEW-Filter. Nach dem Muster des AOSP-Contacts-Manifests (LineageOS lineage-23.2; Deep-Links wie bei der Kalender-App, alle außer Google) übernimmt die App die System-Integrationen: MainActivity trägt jetzt android.intent.category.APP_CONTACTS, ACTION_INSERT/INSERT_OR_EDIT zusätzlich auf dir/person/dir/raw_contact, ACTION_EDIT auf item/person|contact|raw_contact, ACTION_VIEW auf dir/contact|person (Kontaktliste) und NDEF_DISCOVERED (vCard per Beam, Import-Pfad wie bei SEND/VIEW). Ergebnis: QuickContactActivity (android.provider.action.QUICK_CONTACT + com.android.contacts.action.QUICK_CONTACT

Status 2026-09-12 (40)

Secretary Contacts 40 (versionCode 40): Dialer „Neuer Kontakt" (ACTION_INSERT) öffnet den Secretary-Editor. Bisher meldete der Dialer bei eingegebener Nummer + „Neuer Kontakt" „Keine App gefunden", sobald die LineageOS-Kontakte-App deaktiviert war – kein Activity-Filter reagierte auf den Intent.ACTION_INSERT/ACTION_INSERT_OR_EDIT auf der Contacts-URI. Jetzt registriert MainActivity zwei Intent-Filter (mimeType vnd.android.cursor.dir/contact und scheme+host content://com.android.contacts als Fallback ohne Provider-Typauflösung). Die App übernimmt die Extras EXTRA_PHONE_NUMBER/EXTRA_NAME/EXTRA_EMAIL (bzw. ContactsContract.Intents.Insert.*, auch String[]/ArrayList via singleStringExtra) über ContactsViewModel.requestNewContactFromDialer(NewContactPrefill); der NavGraph konsumiert den pending Wunsch (applyDialerPrefill) – auch nach Login, falls nicht eingeloggt – und öffnet den Editor mit vorausgefülltem Telefon (CELL), E-Mail (HOME) und Namen im aktuellen Adressbuch (messenger- Pseudoadressbuch wird übersprungen, Fallback CardDAV→Lokal). launchMode der Activity ist singleTop. versionCode 39 -> 40.

Status 2026-09-11 (39)

Secretary Contacts 39 (versionCode 39): Adressbuch-Konten für den Dialer (DAVx5-Muster) + Upload-Richtung. Der Systemkontakte-Sync nutzt nicht mehr den einzigen Sekundärkonto-Typ org.beowulf.secretary (er war über das SyncAdapter-XML supportsUploading="false" schreibgeschützt – deshalb bot der Android-Anwahlbildschirm beim „Kontakt hinzufügen" nie eine Secretary-Option). Jetzt wird pro CardDAV-Adressbuch ein eigenes Konto vom Typ org.beowulf.secretary.addressbook angelegt (AddressBookAccounts.reconcile, Name = Anzeigename, Serien-/href in den Account-UserData KEY_BOOK_ID/ KEY_BOOK_HREF; userVisible=false → unsichtbar in Android-Einstellungen, aber im Dialer als schreibbares Ziel). Das SyncAdapter-XML heißt jetzt syncadapter_contacts.xml (supportsUploading="true", isAlwaysSyncable, allowParallelSyncs); contacts.xml trägt kein android:accountType mehr (der System-Kontakte-Provider leitet den Typ aus dem SyncAdapter-XML ab); neuer AddressBookAuthenticatorService für den Adressbuch-Konto-Typ. ContactSystemSyncAdapter arbeitet pro Konto/Adressbuch und hat zusätzlich eine Upload-Richtung: im Dialer angelegte/geänderte (DIRTY, DELETED) RawContacts werden als vCard per PUT/DELETE an den Secretary-Server übertragen, im Offline-Cache übernommen und clean markiert. ContactsViewModel reconciled die Konten nach Login/Adressbuch-Operationen und beim Kaltstart und ruft ensureSyncing(context, serverBooks). versionCode 38 -> 39.

Status 2026-09-11 (38)

Secretary Contacts 38 (versionCode 38): Engeres vertikales Padding im Kontakt-Editor. ContactEditScreen ist kompakter: verticalArrangement.spacedBy(16.dp -> 10.dp), die Formular-Karten (Card) haben jetzt contentPadding (horizontal 10 dp, vertikal 8 dp) statt harter Horizontal-Padding von 20 dp und Arrangement.spacedBy(12.dp -> 6.dp) in den Sektionen (Telefon, E-Mail, …). Kein Funktionswechsel, nur engere Abstände. versionCode 37 -> 38.

Status 2026-09-10 (37)

Secretary Contacts 37 (versionCode 37): Dialer-Fix v2: AOSP contacts.xml Attribute + Sync-Timing. contacts.xml: supportsPrefix/supportsSuffix/supportsPhonetic* auf dem name-DataKind (AOSP erwartet diese als true). ensureSyncing() ruft jetzt enablePeriodicSync (inkl. setIsSyncable) VOR dem direkten Sync auf. accountTypeLabel/accountTypeIcon waren ungültige AOSP-Attribute (Build-Fehler) und wurden entfernt. WICHTIG: Nach Installation muss die App force-gestoppt werden, da der System-Kontakte-Provider die Metadaten cached. versionCode 36 -> 37.

Status 2026-09-10 (36)

Secretary Contacts 36 (versionCode 36): Dialer-Fix: contacts.xml Attribute + Sync-Timing. Erweitert das contacts.xml: accountTypeLabel/accountTypeIcon auf ContactsAccountType (Damit der Dialer den Namen/Icon anzeigen kann), supportsPrefix/supportsSuffix/supportsPhonetic* auf dem name-DataKind (AOSP erwartet这些为 true). ensureSyncing() ruft jetzt enablePeriodicSync (inkl. setIsSyncable) VOR dem direkten Sync auf, damit der Account sofort als synchronisierbar markiert ist. versionCode 35 -> 36.

Status 2026-09-10 (35)

Secretary Contacts 35 (versionCode 35): Dialer-Kontaktanlage: accountType in contacts.xml ergänzt. Das <ContactsAccountType>-Element in res/xml/contacts.xml hatte kein android:accountType-Attribut – ohne dieses konnte der System-Kontakte-Provider die EditSchema nicht dem Account-Typ org.beowulf.secretary zuordnen, sodass der Android-Wähler beim Anlegen eines neuen Kontakts keinen Secretary-Account als Ziel anbot. Fix: android:accountType="org.beowulf.secretary" ergänzt. versionCode 34 -> 35.

Status 2026-09-09 (34)

Secretary Contacts 34 (versionCode 34): Adressbuch-Farben + Spezialsammlungen in secretary-common-android v0.20.0. Die lokalen Pseudoadressbücher „Lokal"/„Messenger" und ihre Hinweistexte sind jetzt Teil der gemeinsamen SpecialCollections-Abstraktion (SpecialCollections.localAddressBook()/messengerAddressBook()/ collectionHint(source)); die ContactsViewModel-Getter und der Sammlungs-Screen nutzen sie statt eigener Duplikate. Adressbuch-Farben: CardDAV-Bücher im Langdruck-Menü jetzt mit Eintrag Farbe (gemeinsamer CollectionContextMenu.onColor + ColorSelectDialog); setAddressBookColor setzt die Farbe via CrudManager.setCollectionColor (PROPPATCH calendar-color, der server hinterlegt sie im .metadata und liefert sie im PROPFIND auch für /addressbooks/ zurück) mit Offline-Fallback (setCollectionColorOffline → Write-Behind-Queue). Die Karten zeigen einen Farbpunkt; Adressbuch-Akzentfarbe ist damit auf AddressBook.color abgelegt und übersteht den Offline-Cache-Round-Trip. versionCode 34 == versionName 34.

Status 2026-09-09 (33)

Secretary Contacts 33 (versionCode 33): Adressbuch-Chips entfernt + lokale Sammlungen in der Sammlungsansicht. Die Adressbuch-Pill-Reihe in der Kontaktliste (inkl. „+ Neu") ist entfernt – der Wechsel zwischen Adressbüchern läuft ausschließlich über die neue Sammlungsansicht („Adressbücher"). Die CollectionsScreen listet jetzt alle Adressbücher: die lokalen Pseudoadressbücher „Lokal" und „Messenger" erscheinen als lokale Sammlungen mit Hinweistext („nur auf diesem Gerät, wird nicht synchronisiert"), ohne Server-Kontextmenü (Umbenennen/Kopieren/Löschen weiterhin nur für CardDAV-Bücher); Auswahl öffnet das Buch in der Kontaktliste. versionCode 33 == versionName 33.

Status 2026-09-09 (32)

Secretary Contacts 32 (versionCode 32): Adressbücher-Sammlungsansicht (secretary-common-android v0.19.0). Neue CollectionsScreen (Route collections, Button „Adressbücher" im AppHeader der Kontaktliste): zeigt alle CardDAV-Adressbücher als Karten mit Langdruck-Kontextmenü (Umbenennen / Kopieren / Löschen über CollectionContextMenu), FAB „Adressbuch" zum Anlegen (CreateCollectionDialog), PendingBadge bei offline-pending Änderungen, Offline-/Fehler-Banner sowie Anzeige der Server-Adressbuch-Ids. Auswahl einer Karte öffnet das Adressbuch in der Kontaktliste (Pseudoadressbücher „Lokal"/„Messenger" werden nicht aufgelistet). versionCode 32 == versionName 32.

Status 2026-09-09 (31)

Secretary Contacts 31 (versionCode 31): Adressbücher: Langdruck- Kontextmenü + Kopieren (secretary-common-android v0.19.0). Die Adressbuch-Pills der CardDAV-Bücher haben jetzt ein Langdruck-Menü mit Umbenennen / Kopieren / Löschen (Pseudoadressbücher „Lokal"/ „Messenger" bleiben unverändert). Dazu neue ViewModel-Aktionen renameAddressBook/copyAddressBook/deleteAddressBook. Kopieren legt eine Duplikat-Adressbuch mit allen enthaltenen Kontakten an (CrudManager.copyCollection, Name „<Name> (Kopie)", neue UIDs/hrefs, offline via Write-Behind-Queue). Umbenennen/Anlegen laufen über die gemeinsamen RenameCollectionDialog/CreateCollectionDialog (+ Neu-Pill). versionCode 31 == versionName 31.

Status 2026-09-09 (30)

Secretary Contacts 30 (versionCode 30): Rebuild auf secretary-common-android v0.18.0. Kein App-Code geändert; der APK enthält jetzt die 412-Auto-Recovery (bedingte PUTs mit veraltetem If-Match-ETag holen einmalig die aktuelle Server-ETag und wiederholen den Schreibvorgang statt des „Notiz wurde auf dem Server geaendert"-Fehlers). versionCode 30 == versionName 30.

Status 2026-09-09 (29)

Secretary Contacts 29 (versionCode 29): Adressbuch-Handling in secretary-common-android + Pending-Marker. Das Sammlungs-Handling für Adressbücher (Offline-Merge neu angelegter/gelöschter CardDAV-Bücher und die „wartet auf Synchronisierung"-Marker) lebt jetzt gemeinsam in PendingSync (secretary-common-android v0.17.0). ContactsViewModel.loadAddressBooksInternal merged Server- und lokal bekannte Bücher über PendingSync.mergeCollections (neu angelegte Adressbücher erscheinen sofort, pending-gelöschte verschwinden sofort; Pseudoadressbücher „Lokal"/„Messenger" bleiben als Eigenbau unberührt) und berechnet pendingCollectionIds/pendingItemHrefs aus der Write-Behind-Queue (PendingSync.snapshotState). Die Adressbuch-Chips und Kontaktzeilen zeigen das gemeinsame PendingBadge (SyncProblem-Icon). versionCode 29 == versionName 29.

Status 2026-09-08 (28)

Secretary Contacts 28 (versionCode 28): Konfigurierbares Kontextmenü. Die Aktionen Teilen / Kopieren / Verschieben / Löschen des Langdruck-Menüs lassen sich in den Einstellungen (gemeinsame Sektion „Langdruck-Kontextmenü" aus secretary-common-android v0.16.0) per Checkbox aktivieren/deaktivieren (UiPreferences, Default: alle aktiv). ItemContextMenu blendet deaktivierte Aktionen aus und sperrt das Menü komplett, wenn alle vier aus sind (dann verhält sich die Karte wie eine gewöhnliche klickbare Karte). versionCode 28 == versionName 28.

Status 2026-09-08 (27)

Secretary Contacts 27 (versionCode 27): Gemeinsames Kontextmenü für Kontakte (Kopieren/Verschieben/Teilen/Löschen). Kontaktzeilen haben jetzt das gemeinsame Langdruck-Kontextmenü (ItemContextMenu aus secretary-common-android v0.15.0): Bearbeiten, Teilen (vCard via Share-Sheet, bestehende shareContactAsVcf), Kopieren (Duplikat im selben Adressbuch via CrudManager.copyItem/copyItemOffline – nur CardDAV), Verschieben… (MoveCollectionDialog mit den Server-Adressbüchern; CrudManager.moveItem/moveItemOffline, Cache-Move + System-Sync-Refresh) und Löschen (ConfirmDeleteDialog). Lokale Kontakte erhalten kein Kopieren/Verschieben (kein Server-Ort), Messenger-Kontakte behalten ihren Merge-Flow (kein Kontextmenü). Version 27, versionCode 27 == versionName 27.

Status 2026-09-07 (24)

Secretary Contacts 24 (versionCode 24): Aufgeräumter Header ohne Status-Chip. Der moderne Haupt-Header (ModernAppHeader in secretary-common-android) zeigt keinen Online-Status-Chip mehr; die Aktionsleiste (Sync, Einstellungen) sitzt jetzt rechts oben direkt im Hero-Header. Alle Überschriften verwenden die neue Typewriter-Schrift Special Elite (gebündelt in secretary-common-android, res/font/special_elite.ttf, Apache 2.0). versionCode 24 == versionName 24.

Status 2026-09-01 (22)

Secretary Contacts 22 (versionCode 22): Einheitlicher Moderner Header – alle secretary-Apps teilen jetzt denselben modernen Haupt-Header (ModernAppHeader in secretary-common-android): Hero mit Monogramm + Status-Chip und darunter die identische Aktionsleiste (Sync, Einstellungen). Die Kontaktliste hat damit wieder wie im klassischen Design Sync- und Einstellungen-Buttons direkt im Kopfbereich. versionCode 22 == versionName 22.

Status 2026-09-02 (23)

Secretary Contacts 23 (versionCode 23): Kontakt teilen (vCard) – der Kontakt-Editor hat jetzt für bestehende Kontakte einen „Teilen"-Button (klassisch und modern), der den Kontakt als .vcf über das Android-Share-Sheet versendet. Umsetzung: neue ContactsViewModel.shareContactAsVcf(contact) serialisiert den Kontakt über VCard.serialize in eine Datei unter cacheDir/exports/ und liefert eine FileProvider-Content-URI; die UI startet einen ACTION_SEND-Intent mit MIME text/x-vcard und FLAG_GRANT_READ_URI_PERMISSION. Der Import (ACTION_SEND/VIEW auf .vcf) blieb unverändert erhalten. versionCode 23 == versionName 23.

Status 2026-09-01 (21)

Secretary Contacts 21 (versionCode 21): Komplett modernisierte Oberfläche (Moderner Stil). Kontaktliste, Kontakt-Editor und Einstellungen haben jetzt einen vollständigen „Moderner Stil"-Zweig nach dem gemeinsamen Editorial-Ledger- Design (secretary-common-android): Serifen-Display-Typo, Pill-artige Karten, Hero-Header mit Monogramm, Status-Chips (ONLINE/OFFLINE/SYNC), Adressbuch- und Kategorie-Pills, Pill-Suchfeld, Pill-FAB, moderne Leerszustände; der klassische Stil bleibt unverändert erhalten. Umschaltung über den globalen Schalter „Moderner Stil"; das Theme liefert im modernen Modus die secretaryModernTypography. versionCode 21 == versionName 21.

Status 2026-08-29 (17)

Secretary Contacts 17 (versionCode 17): Sync-Fortschritt: kein Endlos-Spinner mehr nach Abschluss/Fehler – Fix im gemeinsamen SyncProgressUi (secretary-common-android): sobald ein Sync-Job beendet ist (DONE/FAILED), wird der Spinner/Progressbar nicht mehr gezeichnet – es erscheint nur noch der einmalige Toast. Vorher blieb der Status gesetzt und die Inline-SyncProgressUi zeigte bei beendetem/fehlgeschlagenem Sync dauerhaft einen Spinner bei 0/0. Server-Init-Sync läuft jetzt gegen den aktualisierten secretary-server 0.10.0 mit den /sync/*-Endpunkten. versionCode 17 == versionName.

Status 2026-08-28 (14)

Secretary Contacts 14 (versionCode 15): UID-Kollision + Sync-Logging. Bugfix: VCard.parse/parseVCard erzeugte für vCards ohne UID ein Fallback-UID nur aus dem Namen (UUID.nameUUIDFromBytes(fn)); Kontakte mit gleichem Namen kollidierten im System-Sync (Schlüssel uid → existing/wanted), sodass nur einer pro Name geschrieben wurde. Fix: Fallback hashes jetzt Name plus Telefonnummern plus E-Mails (deterministisch → stabil über Parse/Persist/Round-Trip, aber kollisionsarm). Zusätzlich Debug-Logging im ContactSystemSyncAdapter: pro Kontakt insert/update mit uid+Name und am Ende die Kennzahlen cached-Kontakte / distinct-uids / raw-contacts-unter-Account / inserted / updated, um zu prüfen, ob wirklich alle CardDAV/Kontakte in den System-Provider geschrieben werden. Version 14, versionCode 15.

Status 2026-08-28 (13)

Secretary Contacts 13 (versionCode 14): Systemkontakte-Sync korrigiert – der in 12 gebaute Standalone-Kontakte-Provider (authority org.beowulf.secretary.contacts) lieferte in WhatsApp keine Namen, denn WhatsApp/der Dialer lesen ContactsContract.PhoneLookup vom System- Provider com.android.providers.contacts; ein separater Provider mit eigener Authority wird dabei nie konsultiert. Deshalb wurde der Ansatz auf den DAVx5-/secretary-sync-Ansatz umgestellt: Ein SyncAdapter (system/ContactSystemSyncAdapter.kt, Service ContactSystemSyncService) registriert sich für com.android.contacts (res/xml/syncadapter.xml, contentAuthority="com.android.contacts", Konto org.beowulf.secretary) und schreibt RawContacts + Data direkt in den System-Kontakte-Provider (ContentProviderOperation, Datumsschema wie DAVx5: RawContacts.SOURCE_ID/ SYNC1/SYNC2 = UID/ETag; NAME, Phone, Email, Postal, Org, Birthday, Note, Photo). Quelle ist der lokale ContactStore (CardDAV + Lokal, einseitig); Messenger werden übersprungen. res/xml/contacts.xml (ContactsAccountType) wird als android.provider.CONTACTS_STRUCTURE registriert. Trigger: nach Login/Contact-Load/Kaltstart, periodisch (6 h), manuell in den Einstellungen. Version 13, versionCode 14.

Der vorherige separate Provider (Version 12) wurde entfernt (provider/-Paket gelöscht).

Status 2026-08-28 (12)

Secretary Contacts 12 (versionCode 13): Systemkontakte-Provider + SyncAdapter (ersetzt DAVx5). Bisher zeigte die App die Kontakte nur in der eigenen UI; andere Apps (WhatsApp) konnten Namen nicht auflösen, sobald DAVx5 entfernt wurde – nichts schrieb die CardDAV-Kontakte mehr in den System- Kontakte-Provider. Jetzt implementiert die App selbst einen [ContactsContract]-Provider (authority org.beowulf.secretary.contacts) inkl. Contacts, RawContacts, Data, PhoneLookup und Directory. Der SyncAdapter (Service ContactSyncService, res/xml/syncadapter.xml) spiegelt den lokalen ContactStore-Cache (CardDAV + Lokal) über das geteilte Konto (org.beowulf.secretary) in die Provider-Datenbank und baut phone_lookup + Aggregate neu. Problem: separate Authority – von WhatsApp nicht sichtbar; in 13 auf System-Provider-Sync umgestellt.

Status 2026-08-14 (3)

Secretary Contacts 5 (versionCode 5): App-Name auf Contacts verkürzt (wie Notes/Tasks), strings.xml app_name = "Contacts". Version 5 baut auf Version 4 auf (Photo-Data-URI-Parse, ADR-Formatierung, 37/37 Tests grün).

Status 2026-08-14 (2)

Secretary Contacts 4 (versionCode 4): Foto-Darstellung und Adressen gefixt (Fehlerbild: "viele Bilder fehlen", "Adressen beginnen mit ;;"). Ursache (1): der Server speichert 7 von 9 Fotos als PHOTO:data:image/jpeg\;base64\,<b64> (RFC-2397-Data-URI, vCard 4.0 – geschrieben von einem früheren App-Stand); der Parser verwarf diesen Wert (isLikelyBase64 erlaubte keine :/,), die Fotos waren also nicht verloren – sie wurden nur nie angezeigt. Fix: neues extractPhotoB64 streift einen optionalen data:image/<typ>;base64,-Präfix (escaped oder nicht), entfernt Whitespace, ergänzt fehlendes Padding und validiert das Base64; Avatar dekodiert jetzt mit Base64.getMimeDecoder() (Whitespace-tolerant). Ursache (2): ADR-Werte lagen als rohe 7-Komponenten-Form (;;Straße;Ort;Region;PLZ;Land) im Textfeld und serialize escaped die ;-Trennzeichen (Zerstörung beim Round-Trip). Fix: neue formatAddress() formatiert die Komponenten lesbar (Straße, PLZ Ort, Region, Land – ohne ;;), VCard.adrStructured() serialisiert strukturierte ADRs komponentenweise mit rohen ;-Trennzeichen (einfache Eingaben landen in der Straßen-Komponente). ValueList hat einen display-Transform. Neue Tests: Photo-Data-URI (escaped/unescaped/gefaltet), unpadded Base64, ungültiges Foto, Data-URI-Roundtrip, formatAddress, ADR-Serialize-Roundtrip; 37/37 Tests grün. versionCode 4.

Status 2026-08-14

Secretary Contacts 3 (versionCode 3, noch nicht gebaut): Kontaktfotos – ContactAvatar zeigt das Foto (Base64 aus der vCard PHOTO) oder – ohne Foto – die Initialen auf einer deterministisch aus dem Namen abgeleiteten Farbe (Fossify-Stil). Avatare in Listenzeilen, im Merge-Dialog und als zentrierter Header in der Bearbeitungsansicht; dort lässt sich per Tap ein Foto über den System-Photo-Picker (PickVisualMedia) wählen (Kodierung via PhotoCodec: Downsampling ≤ 512 px, JPEG-Qualität 90, Base64) oder wieder entfernen. Formular-Alignment: der freie 90-dp-Typ-Text wurde durch eine ExposedDropdownMenuBox-Typauswahl (132 dp, editierbar, mit Vorschlägen WORK/HOME/MOBILE/FAX/… je Sektion) ersetzt; Wertfelder haben jetzt Labels (Nummer/Adresse/Straße/URL) und feste Spaltenbreiten, Entfernen-Buttons sind einheitlich 40 dp. Datenschicht (VCard-Parse/Serialize, ContactStore, ContactMerge) unterstützte Fotos bereits; neue Tests: VCard-Photo-Roundtrip + ContactStore-Foto-Persistenz.