/* * Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering). * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html */ package at.bitfire.davdroid.resource; import android.accounts.Account; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.os.RemoteException; import android.provider.ContactsContract; import android.provider.ContactsContract.Groups; import android.provider.ContactsContract.RawContacts; import android.support.annotation.NonNull; import java.io.FileNotFoundException; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import at.bitfire.davdroid.App; import at.bitfire.vcard4android.AndroidAddressBook; import at.bitfire.vcard4android.AndroidContact; import at.bitfire.vcard4android.AndroidGroup; import at.bitfire.vcard4android.ContactsStorageException; import lombok.Cleanup; public class LocalAddressBook extends AndroidAddressBook implements LocalCollection { protected static final String SYNC_STATE_CTAG = "ctag", SYNC_STATE_URL = "url"; private final Bundle syncState = new Bundle(); /** * Whether contact groups (LocalGroup resources) are included in query results for * {@link #getAll()}, {@link #getDeleted()}, {@link #getDirty()} and * {@link #getWithoutFileName()}. */ public boolean includeGroups = true; public LocalAddressBook(Account account, ContentProviderClient provider) { super(account, provider, LocalGroup.Factory.INSTANCE, LocalContact.Factory.INSTANCE); } public LocalContact findContactByUID(String uid) throws ContactsStorageException, FileNotFoundException { LocalContact[] contacts = (LocalContact[])queryContacts(LocalContact.COLUMN_UID + "=?", new String[] { uid }); if (contacts.length == 0) throw new FileNotFoundException(); return contacts[0]; } @Override public LocalResource[] getAll() throws ContactsStorageException { List all = new LinkedList<>(); Collections.addAll(all, (LocalResource[])queryContacts(null, null)); if (includeGroups) Collections.addAll(all, (LocalResource[])queryGroups(null, null)); return all.toArray(new LocalResource[all.size()]); } /** * Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0). */ @Override public LocalResource[] getDeleted() throws ContactsStorageException { List deleted = new LinkedList<>(); Collections.addAll(deleted, getDeletedContacts()); if (includeGroups) Collections.addAll(deleted, getDeletedGroups()); return deleted.toArray(new LocalResource[deleted.size()]); } /** * Returns an array of local contacts/groups which have been changed locally (DIRTY != 0). */ @Override public LocalResource[] getDirty() throws ContactsStorageException { List dirty = new LinkedList<>(); Collections.addAll(dirty, getDirtyContacts()); if (includeGroups) Collections.addAll(dirty, getDirtyGroups()); return dirty.toArray(new LocalResource[dirty.size()]); } /** * Returns an array of local contacts which don't have a file name yet. */ @Override public LocalResource[] getWithoutFileName() throws ContactsStorageException { List nameless = new LinkedList<>(); Collections.addAll(nameless, (LocalContact[])queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null)); if (includeGroups) Collections.addAll(nameless, (LocalGroup[])queryGroups(AndroidGroup.COLUMN_FILENAME + " IS NULL", null)); return nameless.toArray(new LocalResource[nameless.size()]); } public void deleteAll() throws ContactsStorageException { try { provider.delete(syncAdapterURI(RawContacts.CONTENT_URI), null, null); provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null); } catch(RemoteException e) { throw new ContactsStorageException("Couldn't delete all local contacts and groups", e); } } public LocalContact[] getDeletedContacts() throws ContactsStorageException { return (LocalContact[])queryContacts(RawContacts.DELETED + "!= 0", null); } public LocalContact[] getDirtyContacts() throws ContactsStorageException { return (LocalContact[])queryContacts(RawContacts.DIRTY + "!= 0 AND " + RawContacts.DELETED + "== 0", null); } public LocalGroup[] getDeletedGroups() throws ContactsStorageException { return (LocalGroup[])queryGroups(Groups.DELETED + "!= 0", null); } public LocalGroup[] getDirtyGroups() throws ContactsStorageException { return (LocalGroup[])queryGroups(Groups.DIRTY + "!= 0 AND " + Groups.DELETED + "== 0", null); } /** * Finds the first group with the given title. If there is no group with this * title, a new group is created. * @param title title of the group to look for * @return id of the group with given title * @throws ContactsStorageException on contact provider errors */ public long findOrCreateGroup(@NonNull String title) throws ContactsStorageException { try { @Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI), new String[] { Groups._ID }, Groups.TITLE + "=?", new String[] { title }, null); if (cursor != null && cursor.moveToNext()) return cursor.getLong(0); ContentValues values = new ContentValues(); values.put(Groups.TITLE, title); Uri uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values); return ContentUris.parseId(uri); } catch(RemoteException e) { throw new ContactsStorageException("Couldn't find local contact group", e); } } public void removeEmptyGroups() throws ContactsStorageException { // find groups without members /** should be done using {@link Groups.SUMMARY_COUNT}, but it's not implemented in Android yet */ for (LocalGroup group : (LocalGroup[])queryGroups(null, null)) if (group.getMembers().length == 0) { App.log.log(Level.FINE, "Deleting group", group); group.delete(); } } public void removeGroups() throws ContactsStorageException { try { provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null); } catch(RemoteException e) { throw new ContactsStorageException("Couldn't remove all groups", e); } } // SYNC STATE @SuppressWarnings("ParcelClassLoader,Recycle") protected void readSyncState() throws ContactsStorageException { @Cleanup("recycle") Parcel parcel = Parcel.obtain(); byte[] raw = getSyncState(); syncState.clear(); if (raw != null) { parcel.unmarshall(raw, 0, raw.length); parcel.setDataPosition(0); syncState.putAll(parcel.readBundle()); } } @SuppressWarnings("Recycle") protected void writeSyncState() throws ContactsStorageException { @Cleanup("recycle") Parcel parcel = Parcel.obtain(); parcel.writeBundle(syncState); setSyncState(parcel.marshall()); } public String getURL() throws ContactsStorageException { synchronized (syncState) { readSyncState(); return syncState.getString(SYNC_STATE_URL); } } public void setURL(String url) throws ContactsStorageException { synchronized (syncState) { readSyncState(); syncState.putString(SYNC_STATE_URL, url); writeSyncState(); } } @Override public String getCTag() throws ContactsStorageException { synchronized (syncState) { readSyncState(); return syncState.getString(SYNC_STATE_CTAG); } } @Override public void setCTag(String cTag) throws ContactsStorageException { synchronized (syncState) { readSyncState(); syncState.putString(SYNC_STATE_CTAG, cTag); writeSyncState(); } } // HELPERS public static void onRenameAccount(@NonNull ContentResolver resolver, @NonNull String oldName, @NonNull String newName) throws RemoteException { @Cleanup("release") ContentProviderClient client = resolver.acquireContentProviderClient(ContactsContract.AUTHORITY); if (client != null) { ContentValues values = new ContentValues(1); values.put(RawContacts.ACCOUNT_NAME, newName); client.update(RawContacts.CONTENT_URI, values, RawContacts.ACCOUNT_NAME + "=?", new String[]{oldName}); } } }