use pbap_core::client::PbapClient;
use pbap_core::phonebook::PhonebookPath;
use pbap_core::PhonebookMetadata;
use store::{NewContact, PbapMeta, Store};
use tokio::io::{AsyncRead, AsyncWrite};
use crate::util::now_ms;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncReport {
UpToDate,
Refreshed(Refresh),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Refresh {
pub listed: usize,
pub pull_failed: usize,
pub no_uid: usize,
pub written: usize,
pub wiped: bool,
}
fn hex16(bytes: [u8; 16]) -> String {
use std::fmt::Write as _;
let mut s = String::with_capacity(32);
for b in bytes {
let _ = write!(s, "{b:02x}");
}
s
}
fn to_pbap_meta(m: &PhonebookMetadata) -> PbapMeta {
PbapMeta {
database_id: m.database_id.map(hex16),
primary_version: m.primary_version.map(hex16),
secondary_version: m.secondary_version.map(hex16),
}
}
async fn refresh_contacts<T: AsyncRead + AsyncWrite + Unpin>(
client: &mut PbapClient<T>,
store: &Store,
path: PhonebookPath,
wiped: bool,
) -> anyhow::Result<Refresh> {
let entries = client.list(path, None, 0).await?;
let listed = entries.len();
let mut pull_failed = 0usize;
let mut fetched = Vec::with_capacity(listed);
for entry in &entries {
if entry.handle() == "0.vcf" {
continue;
}
match client.pull(path, entry.handle()).await {
Ok(contact) => fetched.push(contact),
Err(e) => {
pull_failed = pull_failed.saturating_add(1);
tracing::warn!("pbap pull {} failed, skipping: {e}", entry.handle());
}
}
}
let pulled = fetched.len();
let new_contacts: Vec<NewContact> = fetched
.into_iter()
.filter_map(|c| {
let phones = c.phones().to_vec();
Some(NewContact { uid: c.uid?, display_name: c.display_name, phones })
})
.collect();
let written = new_contacts.len();
store.upsert_contacts(new_contacts).await?;
Ok(Refresh { listed, pull_failed, no_uid: pulled.saturating_sub(written), written, wiped })
}
pub async fn sync_contacts<T: AsyncRead + AsyncWrite + Unpin>(
client: &mut PbapClient<T>,
store: &Store,
path: PhonebookPath,
) -> anyhow::Result<SyncReport> {
let remote = to_pbap_meta(&client.phonebook_metadata(path).await?);
let cached = store.pbap_meta().await?;
if remote.database_id.is_some() && remote == cached {
store.set_meta("contacts_synced", "true").await?;
store.set_contacts_synced_at(now_ms()).await?;
return Ok(SyncReport::UpToDate);
}
let cleared = remote.database_id.is_some() && remote.database_id != cached.database_id;
if cleared {
store.clear_contacts().await?;
}
let wiped = cleared && cached.database_id.is_some();
let refresh = refresh_contacts(client, store, path, wiped).await?;
store.set_pbap_meta(&remote).await?;
store.set_meta("contacts_synced", "true").await?;
store.set_contacts_synced_at(now_ms()).await?;
Ok(SyncReport::Refreshed(refresh))
}
#[cfg(test)]
mod tests;