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:
- Die Ursache beseitigt.
ContactSystemSyncAdapterkodiert das vom Provider gelesene Foto jetzt mitBase64.NO_WRAPstattBase64.DEFAULT(Zeile 585ff).PhotoCodec.encodenutzte bereitsBase64.getEncoder(), also einwandfrei; nur der Upload-Pfad aus dem Provider war befallen. fold()ist jetzt belastbar.VCard.foldzerlegt 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.- Der Parser repariert, statt zu truncaten.
VCard.unfoldfuegt unmarkierte Base64-Fortsetzungszeilen einerPHOTO;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; einPHOTO;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:
- Einmal prüfen statt N-mal scheitern.
ContactSystemSyncAdapter.hasWritePermission(Companion) am Anfang vonsyncLocked: ohne die Berechtigung eine Zeile Log mit Ursache und Abbruch,numAuthExceptions += 1, kein Serverabruf und kein Insert-Sturm. Dascatch (e: Exception)pro Kontakt bleibt fuer echte Transportfehler stehen, kann eine fehlende Berechtigung aber nicht mehr als "0 inserted" tarnen. - Fragen, wo gefragt werden kann.
MainActivity.onCreatestartet denRequestPermission-Launcher fuerWRITE_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. - Nach der Freigabe sofort spiegeln.
ContactsViewModel.onSystemWritePermissionChangedruftContactSystemScheduler.rearmauf; da nie etwas geschrieben wurde, fuellt der erste Lauf mit der Berechtigung den Provider in einem Zug. Bewusstrearmund nichtensureSyncing: die Antwort aus dem Systemdialog kommt zurueck, bevor die CardDAV-Sammlungen geladen sind, undreconcileentfernt 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 anrearm, und der Buchpfad reconciliert beim Laden von sich aus. - Der Zustand wird sichtbar.
systemWritePermissionGrantedin 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:
- Kein Buch im Provider löschen, nur weil nichts geholt werden konnte.
refreshFromServergibt jetztRefresh.SYNCED/FAILED/UNKNOWN_BOOKzurück. NurSYNCEDdarf ein leeres Buch melden; beiFAILED/UNKNOWN_BOOKund leerem Cache lässtsyncLockedden Provider unangetastet und loggt es. Der Upload (DIRTY/DELETED) läuft weiterhin, ist aber vom Mirror entkoppelt. - Unbekanntes Buch wird beim Server nachgeschlagen, nicht übersprungen.
snap.collectionsschreibt 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.refreshFromServerholt 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. - UID-Fallback wieder Namens+Telefon+E-Mail (Regression).
VCard.parsebildete die Fallback-UID nur ausFN(„unknown" ohneFN), 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 ohneUIDteilten sich eine uid, wurden im selben Lauf doppelt eingefügt (dieexisting-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), unddedupeByUidlässt Kollisionen nicht mehr mehrfach schreiben. Phone.NORMALIZED_NUMBERaus E.164. WhatsApp löst Namen ausschließlich überContactsContract.PhoneLookupauf, und diephone_lookup-Tabelle entsteht ausData.NUMBER/NORMALIZED_NUMBER. Bisher ging der rohe vCard-TELungefiltert inNUMBER–Tel:-Präfix,;ext=3oder die00-Präfixform liefern dort keinen brauchbaren Schlüssel, der Kontakt liegt dann im System, WhatsApp zeigt aber nur die Nummer. Neu istsystem/PhoneNumbers(reines Kotlin, ohneandroid.telephonyund ohne Region): es entferntTel:/Fax:und<>, trennt die Nebenstelle ab (;ext=,;3,ext. 3,x89,#3), wandelt00in+und schreibt die E.164-Nummer zusätzlich alsNORMALIZED_NUMBER. Bewusst konservativ: nur reine Ziffernwerte mit+/00werden normalisiert, weil eine trunk-präfix-behaftete Form wie+49 (030) 12 34-567sich 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 (VCardTestFallback-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
- kinetisch + endlos), Adressen lesbar**. (1) Das in 50 durch eine
LazyColumnersetzte Cover-Flow-Karussell (VerticalPager) ist zurück, aber in der schlanken Variante: nur noch 2D-Effekte (translationYfür den Peek-Strip + leichte Skalierung, kein 3D-rotationX, kein Alpha-Fade) – die Seitenabstände liest ausschließlich dergraphicsLayerin der Zeichnungsphase,zIndexhängt nur am selten wechselndencurrentPage. So bleibt das Scrollen beim weit vorne gestarteten Endlos-Pager (loopBase = size * 1000,Math.floorMod,pageCount = Int.MAX_VALUE) flüssig, während ein kräftiger Wurf kinetisch über bis zu drei Karten trägt (PagerSnapDistance.atMost(3),snapPositionalThreshold = 0.8). (2) Die Adresszeile der Karte zeigt nicht mehr die rohe RFC-2426-7-Komponenten-Form mit;;, sondern die lesbare Formatierung überformatAddress()(Straße, PLZ Ort, Region, Land – wie im Editor). (3) Das vCard-Layout aus 50 (Name bricht um statt gekürzt, Rolle, Typ-Token rechts, Trenner, mehrzeilige Adresse unten) bleibt erhalten. Alle Tests grün. versionCode 50 -> 51.
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
ContactsCodec): Server-Abruf, Rehome-Upload lokaler UIDs, Pending-Delete-Filter, Prepare-Null-Erhalt und Cache-Ersatz liegen in common; die lokalemergeWithPending-Logik wurde entfernt. Offline-Verhalten (goOffline+ Cache-Anzeige beiIOException,DavExceptionals echter Fehler) unverändert; nach dem Merge aktualisierencachedByBook,ContactsCodec-Cache,flushPending()undContactSystemScheduler.ensureSyncingweiter den System-Kontakte-Provider. Lokale/Messenger-Bücher (cachedByBook) sind unberührt. View bleibt beim Sync stabil (kein Leeren/Neubau). Zusätzlich zeigt die Kontaktliste den Vollbild-Spinner nur noch bei leerer Liste (loading && filtered.isEmpty(), wie Tasks/Notes/Links) statt bei jedem Sync – die vorhandenen Kontakte bleiben während des Hintergrund-Syncs sichtbar (ContactListScreen). versionCode 43 -> 44.
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
- ACTION_VIEW auf den item-MIMEs; Dialog,
excludeFromRecents; Anruf über ACTION_DIAL statt ACTION_CALL – keine CALL_PHONE-Berechtigung; „Bearbeiten" leitet als ACTION_EDIT anMainActivity). ShowOrCreateActivity (com.android.contacts.action.SHOW_OR_CREATE_CONTACT, tel/mailto): zeigt passende vorhandene Kontakte (PhoneLookup bzw. E-Mail-Suche) und „Neuer Kontakt" mit vorausgefüllter Nummer/E-Mail. ContactPickerActivity (ACTION_PICK/GET_CONTENT auf den Kontakt-MIMEs): listet die secretary- Kontakte aus dem System-Provider und liefert die gewählteContacts-URI. Neuesdata/SystemContactLookupübersetzt System-URIs (contacts/<id>,as_vcard/<id>,lookup/<key>,raw_contacts/<id>,data/<id>/QuickContact-Pfade) in die secretary-UID (RawContacts.SOURCE_ID unterorg.beowulf.secretary.addressbook/org.beowulf.secretary») und zurück; derContactsViewModellöst peropenContactFromUrieinen bestehenden Kontakt auf und öffnet überpendingOpenContact(NavGraph- Effekt, login-geguardet) den Editor –MainActivity.handleInsertContactöffnet bei konkreter Kontakt-URI (INSERT_OR_EDIT) stattdessen den bestehenden Kontakt.android:appCategory="social", Dialog-ThemeTheme.SecretaryContacts.Dialog. **KEIN Widget**: das AOSP- Kontakte-Manifest registriert keins (anders als der Kalender mit Etar);CREATE_SHORTCUT`-Aliase sind bewusst nicht übernommen (Nischen-Flow). versionCode 40 -> 41.
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.