/*
* Funambol is a mobile platform developed by Funambol, Inc.
* Copyright (C) 2010 Funambol, Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, see http://www.gnu.org/licenses or write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA.
*
* You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite
* 305, Redwood City, CA 94063, USA, or at email address info@funambol.com.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by Funambol" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by Funambol".
*/
package de.chbosync.android.syncmlclient.source.pim.contact;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Vector;
import android.content.ContentProviderOperation;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import com.funambol.util.Log;
import de.chbosync.android.syncmlclient.AndroidCustomization;
import de.chbosync.android.syncmlclient.source.AbstractDataManager;
public class GroupManager extends AbstractDataManager<Group> {
private static final String TAG_LOG = "GroupManager";
private static final Uri GROUPS_URI = ContactsContract.Groups.CONTENT_URI;
private static final String FUNAMBOL_SOURCE_ID_PREFIX = "funambol-";
protected AndroidCustomization customization = AndroidCustomization.getInstance();
protected boolean callerIsSyncAdapter = true;
protected int allItemsCount = 0;
// Constructors------------------------------------------------
public GroupManager(Context context) {
this(context, true);
}
public GroupManager(Context context, boolean callerIsSyncAdapter) {
super(context);
this.callerIsSyncAdapter = callerIsSyncAdapter;
}
protected String getAuthority() {
return ContactManager.CONTACTS_AUTHORITY;
}
@Override
public Group load(String key) throws IOException {
long id;
try {
id = Long.parseLong(key);
} catch (Exception e) {
Log.error(TAG_LOG, "Invalid key: " + key, e);
throw new IOException("Invalid key " + key);
}
Uri uri = addCallerIsSyncAdapterFlag(ContactsContract.Groups.CONTENT_URI);
uri = ContentUris.withAppendedId(uri, id);
Cursor cursor = null;
try {
cursor = resolver.query(uri, null, null, null, null);
if (!cursor.moveToFirst()) {
throw new IOException("Item not found " + key);
}
// Load the fields we handle
String title = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Groups.TITLE));
String notes = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Groups.NOTES));
String systemId = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Groups.SYSTEM_ID));
Group g = new Group();
g.setTitle(title);
g.setNotes(notes);
g.setSystemId(systemId);
return g;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
@Override
public String add(Group item) throws IOException {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Saving group");
}
Uri uri = addCallerIsSyncAdapterFlag(ContactsContract.Groups.CONTENT_URI);
// Create the content value
ContentValues cv = prepareAllFields(item);
// Set the group as visible by default
cv.put(ContactsContract.Groups.GROUP_VISIBLE, 1);
// Now create the contact with a single batch operation
try {
uri = resolver.insert(uri, cv);
// The first insert is the one generating the ID for this contact
long id = ContentUris.parseId(uri);
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "The new group has id: " + id);
}
item.setId(id);
return "" + id;
} catch (Exception e) {
Log.error(TAG_LOG, "Cannot create group ", e);
throw new IOException("Cannot create group in db");
}
}
@Override
public void update(String key, Group item) throws IOException {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Updating group: " + key);
}
long groupId;
try {
groupId = Long.parseLong(key);
} catch(Exception e) {
Log.error(TAG_LOG, "Invalid group key " + key, e);
throw new IOException("Invalid group key");
}
// If the contact does not exist, then we perform an add
if (!exists(key)) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Tried to update a non existing group. Creating a new one ");
}
add(item);
return;
}
// Set the id
item.setId(groupId);
// Prepare the new fields and update
Uri uri = addCallerIsSyncAdapterFlag(ContactsContract.Groups.CONTENT_URI);
uri = ContentUris.withAppendedId(uri, groupId);
ContentValues cv = prepareAllFields(item);
resolver.update(uri, cv, null, null);
}
@Override
public void delete(String key) throws IOException {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Deleting group: " + key);
}
long groupId;
try {
groupId = Long.parseLong(key);
} catch (Exception e) {
Log.error(TAG_LOG, "Invalid group id " + key, e);
throw new IOException("Invalid group id");
}
int count = hardDelete(groupId);
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(TAG_LOG, "Deleted group count: " + count);
}
if (count < 1) {
Log.error(TAG_LOG, "Cannot delete groups: " + groupId);
throw new IOException("Cannot delete groups: " + groupId);
}
}
@Override
public void deleteAll() throws IOException {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Deleting all groups");
}
Uri uri = addCallerIsSyncAdapterFlag(ContactsContract.Groups.CONTENT_URI);
// Delete from groups
// Note: delete only contacts from funambol accounts
int count = resolver.delete(uri,
ContactsContract.Groups.ACCOUNT_TYPE+"='"+accountType+"'", null);
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(TAG_LOG, "Deleted groups count: " + count);
}
if (count < 0) {
Log.error(TAG_LOG, "Cannot delete all groups");
throw new IOException("Cannot delete groups");
}
}
@Override
public boolean exists(String key) {
long id;
try {
id = Long.parseLong(key);
} catch (Exception e) {
Log.error(TAG_LOG, "Invalid item key " + key, e);
return false;
}
String cols[] = {ContactsContract.Groups._ID, ContactsContract.Groups.DELETED};
Uri uri = ContentUris.withAppendedId(ContactsContract.Groups.CONTENT_URI, id);
Cursor cur = resolver.query(uri, cols, null, null, null);
boolean found;
if (!cur.moveToFirst()) {
found = false;
} else {
int deleted = cur.getInt(1);
if (deleted == 0) {
found = true;
} else {
found = false;
}
}
cur.close();
return found;
}
@Override
public Enumeration getAllKeys() throws IOException {
String cols[] = {ContactsContract.Groups._ID};
StringBuffer whereClause = new StringBuffer();
if (accountName != null) {
whereClause.append(ContactsContract.Groups.ACCOUNT_NAME).append("='").append(accountName).append("'");
whereClause.append(" AND ");
whereClause.append(ContactsContract.Groups.ACCOUNT_TYPE).append("='").append(accountType).append("'");
whereClause.append(" AND ");
}
whereClause.append(ContactsContract.Groups.DELETED).append("=").append("0");
Cursor groupsCur = resolver.query(ContactsContract.Groups.CONTENT_URI,
cols, whereClause.toString(), null, null);
try {
int groupsSize = groupsCur.getCount();
Vector<String> itemKeys = new Vector<String>(groupsSize);
if (!groupsCur.moveToFirst()) {
return itemKeys.elements();
}
for (int i = 0; i < groupsSize; i++) {
String key = groupsCur.getString(0);
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Found item with key: " + key);
}
itemKeys.addElement(key);
groupsCur.moveToNext();
}
allItemsCount = groupsSize;
return itemKeys.elements();
} catch (Exception e) {
Log.error(TAG_LOG, "Cannot get all items keys: ", e);
throw new IOException("Cannot get all items keys");
} finally {
groupsCur.close();
}
}
@Override
public int getAllCount() throws IOException {
return allItemsCount;
}
public Vector<com.funambol.syncml.protocol.Property> getSupportedProperties() {
Vector<com.funambol.syncml.protocol.Property> properties =
new Vector<com.funambol.syncml.protocol.Property>();
return properties;
}
public Vector commit() {
return null;
}
protected String getGroupLuid(String id) {
return "G" + id;
}
protected String getGroupId(String luid) {
if (luid.charAt(0) != 'G') {
// This is not expected
throw new IllegalArgumentException("Illegal group luid " + luid);
} else {
return luid.substring(1);
}
}
protected Uri addCallerIsSyncAdapterFlag(Uri uri) {
if(callerIsSyncAdapter) {
Uri.Builder b = uri.buildUpon();
b.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true");
return b.build();
} else {
return uri;
}
}
/**
* Hard delete the group from the store
* @param rawContactId
* @return
*/
protected int hardDelete(long groupId) {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Hard deleting group: " + groupId);
}
Uri uri = addCallerIsSyncAdapterFlag(ContactsContract.Groups.CONTENT_URI);
// Delete from raw_contacts (related rows in Data table are
// automatically deleted)
return resolver.delete(uri,
ContactsContract.Groups._ID+"="+groupId, null);
}
private ContentValues prepareAllFields(Group group) {
ContentValues cv = new ContentValues();
String title = group.getTitle();
String notes = group.getNotes();
String systemId = group.getSystemId();
if (title != null) {
cv.put(ContactsContract.Groups.TITLE, title);
}
if (notes != null) {
cv.put(ContactsContract.Groups.NOTES, notes);
}
if (systemId != null) {
cv.put(ContactsContract.Groups.SYSTEM_ID, systemId);
}
// Add the account name and account type
cv.put(ContactsContract.Groups.ACCOUNT_NAME, accountName);
cv.put(ContactsContract.Groups.ACCOUNT_TYPE, accountType);
return cv;
}
protected void linkContactsToGroup(String groupId, List<String> contacts) throws IOException {
// The association is in the contacts table where we have one row per
// group
// We must add a row for each contact in the Data table.
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
Uri uri = addCallerIsSyncAdapterFlag(ContactsContract.Data.CONTENT_URI);
try {
for(String contactId : contacts) {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Creating query to associate " + contactId + " to group " + groupId);
}
ContentProviderOperation op = ContentProviderOperation.newInsert(uri)
.withValue(ContactsContract.Data.RAW_CONTACT_ID, contactId)
.withValue(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID, groupId)
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE)
.build();
ops.add(op);
}
resolver.applyBatch(ContactsContract.AUTHORITY, ops);
} catch (Exception e) {
Log.error(TAG_LOG, "Cannot link contacts to group", e);
throw new IOException("Cannot link contacts to group");
}
}
protected void unlinkContactsFromGroup(String groupId) throws IOException {
// The association is in the contacts table where we have one row per
// group
// We must delete all the rows of this account where the mime type is
// the groupmembership and the groupid is the given one
StringBuffer whereClause = new StringBuffer();
whereClause.append(ContactsContract.Data.MIMETYPE).append("='")
.append(ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE).append("'")
.append(" AND ")
.append(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID).append("='")
.append(groupId).append("'");
resolver.delete(ContactsContract.Data.CONTENT_URI, whereClause.toString(), null);
}
}