/*
* Kontalk Android client
* Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* 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 General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kontalk.sync;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.util.StringUtils;
import org.jxmpp.util.XmppStringUtils;
import org.spongycastle.openpgp.PGPPublicKey;
import android.accounts.Account;
import android.accounts.OperationCanceledException;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SyncResult;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Process;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import org.kontalk.BuildConfig;
import org.kontalk.Log;
import org.kontalk.R;
import org.kontalk.authenticator.Authenticator;
import org.kontalk.crypto.PGP;
import org.kontalk.crypto.PGPUserID;
import org.kontalk.data.Contact;
import org.kontalk.provider.Keyring;
import org.kontalk.provider.MyUsers;
import org.kontalk.provider.MyUsers.Users;
import org.kontalk.service.msgcenter.MessageCenterService;
import org.kontalk.util.XMPPUtils;
/**
* The syncer core.
* @author Daniele Ricci
*/
public class Syncer {
// using SyncAdapter tag
private static final String TAG = SyncAdapter.TAG;
// max time to wait for network response
private static final int MAX_WAIT_TIME = 60000;
/** {@link Data} column for the display name. */
public static final String DATA_COLUMN_DISPLAY_NAME = Data.DATA1;
/** {@link Data} column for the account name. */
public static final String DATA_COLUMN_ACCOUNT_NAME = Data.DATA2;
/** {@link Data} column for the phone number. */
public static final String DATA_COLUMN_PHONE = Data.DATA3;
/** {@link RawContacts} column for the display name. */
public static final String RAW_COLUMN_DISPLAY_NAME = RawContacts.SYNC1;
/** {@link RawContacts} column for the phone number. */
public static final String RAW_COLUMN_PHONE = RawContacts.SYNC2;
/** {@link RawContacts} column for the JID. */
public static final String RAW_COLUMN_USERID = RawContacts.SYNC3;
/** Random packet id used for requesting public keys. */
static final String IQ_PACKET_ID = StringUtils.randomString(10);
private volatile boolean mCanceled;
private final Context mContext;
private final static class PresenceItem {
public String from;
public String status;
public String rosterName;
public long timestamp;
public byte[] publicKey;
public boolean blocked;
public boolean presence;
/** True if found during roster match. */
public boolean matched;
/** Discard this entry: it has not been found on server. */
public boolean discarded;
}
// FIXME this class should handle most recent/available presence stanzas
private static final class PresenceBroadcastReceiver extends BroadcastReceiver {
/** Max number of items in a roster match request. */
private static final int MAX_ROSTER_MATCH_SIZE = 500;
private List<PresenceItem> response;
private final WeakReference<Syncer> notifyTo;
private final List<String> jidList;
private int rosterParts = -1;
private String[] iq;
private String presenceId;
private int presenceCount;
private int pubkeyCount;
private int rosterCount;
/** Packet id list for not matched contacts (in roster but not matched on server). */
private Set<String> notMatched = new HashSet<>();
private boolean blocklistReceived;
public PresenceBroadcastReceiver(List<String> jidList, Syncer notifyTo) {
this.notifyTo = new WeakReference<>(notifyTo);
this.jidList = jidList;
}
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (MessageCenterService.ACTION_PRESENCE.equals(action)) {
// consider only presences received *after* roster response
if (response != null && presenceId != null) {
String jid = intent.getStringExtra(MessageCenterService.EXTRA_FROM);
String type = intent.getStringExtra(MessageCenterService.EXTRA_TYPE);
String id = intent.getStringExtra(MessageCenterService.EXTRA_PACKET_ID);
if (type != null && presenceId.equals(id)) {
// update presence item data
String bareJid = XmppStringUtils.parseBareJid(jid);
PresenceItem item = getPresenceItem(bareJid);
item.status = intent.getStringExtra(MessageCenterService.EXTRA_STATUS);
item.timestamp = intent.getLongExtra(MessageCenterService.EXTRA_STAMP, -1);
item.rosterName = intent.getStringExtra(MessageCenterService.EXTRA_ROSTER_NAME);
if (!item.presence) {
item.presence = true;
// increment presence count
presenceCount++;
// check user existance (only if subscription is "both")
if (!item.matched && intent.getBooleanExtra(MessageCenterService.EXTRA_SUBSCRIBED_FROM, false) &&
intent.getBooleanExtra(MessageCenterService.EXTRA_SUBSCRIBED_TO, false)) {
// verify actual user existance through last activity
String lastActivityId = StringUtils.randomString(6);
MessageCenterService.requestLastActivity(context, item.from, lastActivityId);
notMatched.add(lastActivityId);
}
}
}
}
}
// roster match result received
else if (MessageCenterService.ACTION_ROSTER_MATCH.equals(action)) {
String id = intent.getStringExtra(MessageCenterService.EXTRA_PACKET_ID);
for (String iqId : iq) {
if (iqId.equals(id)) {
// decrease roster parts counter
rosterParts--;
String[] list = intent.getStringArrayExtra(MessageCenterService.EXTRA_JIDLIST);
if (list != null) {
rosterCount += list.length;
if (response == null) {
// prepare list to be filled in with presence data
response = new ArrayList<>(rosterCount);
}
for (String jid : list) {
PresenceItem p = new PresenceItem();
p.from = jid;
p.matched = true;
response.add(p);
}
}
if (rosterParts <= 0) {
// all roster parts received
if (rosterCount == 0 && blocklistReceived) {
// no roster elements
finish();
}
else {
Syncer w = notifyTo.get();
if (w != null) {
// request presence data for the whole roster
presenceId = StringUtils.randomString(6);
w.requestPresenceData(presenceId);
// request public keys for the whole roster
w.requestPublicKeys();
// request block list
w.requestBlocklist();
}
}
}
// no need to continue
break;
}
}
}
else if (MessageCenterService.ACTION_PUBLICKEY.equals(action)) {
if (response != null) {
String jid = intent.getStringExtra(MessageCenterService.EXTRA_FROM);
// see if bare JID is present in roster response
String compare = XmppStringUtils.parseBareJid(jid);
for (PresenceItem item : response) {
if (XmppStringUtils.parseBareJid(item.from).equalsIgnoreCase(compare)) {
item.publicKey = intent.getByteArrayExtra(MessageCenterService.EXTRA_PUBLIC_KEY);
// increment vcard count
pubkeyCount++;
break;
}
}
// done with presence data and blocklist
if (pubkeyCount == presenceCount && blocklistReceived && notMatched.size() == 0)
finish();
}
}
else if (MessageCenterService.ACTION_BLOCKLIST.equals(action)) {
blocklistReceived = true;
String[] list = intent.getStringArrayExtra(MessageCenterService.EXTRA_BLOCKLIST);
if (list != null) {
for (String jid : list) {
// see if bare JID is present in roster response
String compare = XmppStringUtils.parseBareJid(jid);
for (PresenceItem item : response) {
if (XmppStringUtils.parseBareJid(item.from).equalsIgnoreCase(compare)) {
item.blocked = true;
break;
}
}
}
}
// done with presence data and blocklist
if (pubkeyCount >= presenceCount && notMatched.size() == 0)
finish();
}
// last activity (for user existance verification)
else if (MessageCenterService.ACTION_LAST_ACTIVITY.equals(action)) {
String requestId = intent.getStringExtra(MessageCenterService.EXTRA_PACKET_ID);
if (notMatched.contains(requestId)) {
notMatched.remove(requestId);
String type = intent.getStringExtra(MessageCenterService.EXTRA_TYPE);
// consider only item-not-found (404) errors
if (type != null && type.equalsIgnoreCase(IQ.Type.error.toString()) &&
XMPPError.Condition.item_not_found.toString().equals(intent
.getStringExtra(MessageCenterService.EXTRA_ERROR_CONDITION))) {
// user does not exist!
String jid = intent.getStringExtra(MessageCenterService.EXTRA_FROM);
// discard entry
discardPresenceItem(jid);
// unsubscribe!
unsubscribe(context, jid);
if (pubkeyCount >= presenceCount && blocklistReceived && notMatched.size() == 0)
finish();
}
}
}
// connected! Retry...
else if (MessageCenterService.ACTION_CONNECTED.equals(action) && rosterParts < 0) {
Syncer w = notifyTo.get();
if (w != null) {
// request a roster match
rosterParts = getRosterParts(jidList);
iq = new String[rosterParts];
for (int i = 0; i < rosterParts; i++) {
int end = (i+1)*MAX_ROSTER_MATCH_SIZE;
if (end >= jidList.size())
end = jidList.size();
List<String> slice = jidList.subList(i*MAX_ROSTER_MATCH_SIZE, end);
iq[i] = StringUtils.randomString(6);
w.requestRosterMatch(iq[i], slice);
}
}
}
}
private void discardPresenceItem(String jid) {
for (PresenceItem item : response) {
if (XmppStringUtils.parseBareJid(item.from).equalsIgnoreCase(jid)) {
item.discarded = true;
return;
}
}
}
private PresenceItem getPresenceItem(String jid) {
for (PresenceItem item : response) {
if (XmppStringUtils.parseBareJid(item.from).equalsIgnoreCase(jid))
return item;
}
// add item if not found
PresenceItem item = new PresenceItem();
item.from = jid;
response.add(item);
return item;
}
private void unsubscribe(Context context, String jid) {
Intent i = new Intent(context, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_PRESENCE);
i.putExtra(MessageCenterService.EXTRA_TO, jid);
i.putExtra(MessageCenterService.EXTRA_TYPE, Presence.Type.unsubscribe.name());
context.startService(i);
}
private int getRosterParts(List<String> jidList) {
return (int) Math.ceil((double) jidList.size() / MAX_ROSTER_MATCH_SIZE);
}
public List<PresenceItem> getResponse() {
return (rosterCount >= 0) ? response : null;
}
private void finish() {
Syncer w = notifyTo.get();
if (w != null) {
synchronized (w) {
w.notifyAll();
}
}
}
}
public Syncer(Context context) {
mContext = context;
}
public void onSyncCanceled() {
mCanceled = true;
}
public void onSyncResumed() {
mCanceled = false;
}
private static final class RawPhoneNumberEntry {
public final String number;
public final String jid;
public final String lookupKey;
public RawPhoneNumberEntry(String lookupKey, String number, String jid) {
this.lookupKey = lookupKey;
this.number = number;
this.jid = jid;
}
}
/**
* The actual sync procedure.
* This one uses the slowest method ever: it first checks for every phone
* number in all contacts and it sends them to the server. Once a response
* is received, it deletes all the raw contacts created by us and then
* recreates only the ones the server has found a match for.
*/
public void performSync(Context context, Account account, String authority,
ContentProviderClient provider, ContentProviderClient usersProvider,
SyncResult syncResult)
throws OperationCanceledException {
final Map<String, RawPhoneNumberEntry> lookupNumbers = new HashMap<>();
final List<String> jidList = new ArrayList<>();
// resync users database
Log.v(TAG, "resyncing users database");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// update users database
Uri uri = Users.CONTENT_URI.buildUpon()
.appendQueryParameter(Users.RESYNC, "true")
.build();
try {
int count = usersProvider.update(uri, new ContentValues(), null, null);
Log.d(TAG, "users database resynced (" + count + ")");
}
catch (Exception e) {
Log.e(TAG, "error resyncing users database - aborting sync", e);
syncResult.databaseError = true;
return;
}
// query all contacts
Cursor cursor;
try {
cursor = usersProvider.query(Users.CONTENT_URI_OFFLINE,
new String[] { Users.JID, Users.NUMBER, Users.LOOKUP_KEY },
null, null, null);
}
catch (Exception e) {
Log.e(TAG, "error querying users database - aborting sync", e);
syncResult.databaseError = true;
return;
}
while (cursor.moveToNext()) {
if (mCanceled) {
cursor.close();
throw new OperationCanceledException();
}
String jid = cursor.getString(0);
String number = cursor.getString(1);
String lookupKey = cursor.getString(2);
// avoid to send duplicates to the server
if (lookupNumbers.put(XmppStringUtils.parseLocalpart(jid),
new RawPhoneNumberEntry(lookupKey, number, jid)) == null)
jidList.add(jid);
}
cursor.close();
if (mCanceled) throw new OperationCanceledException();
// empty contacts :-|
if (jidList.size() == 0) {
// delete all Kontalk raw contacts
try {
syncResult.stats.numDeletes += deleteAll(account, provider);
}
catch (Exception e) {
Log.e(TAG, "contact delete error", e);
syncResult.databaseError = true;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
try {
syncResult.stats.numDeletes += deleteProfile(account, provider);
}
catch (Exception e) {
Log.e(TAG, "profile delete error", e);
syncResult.databaseError = true;
}
}
commit(usersProvider, syncResult);
}
else {
final LocalBroadcastManager lbm = LocalBroadcastManager
.getInstance(mContext);
// register presence broadcast receiver
PresenceBroadcastReceiver receiver = new PresenceBroadcastReceiver(jidList, this);
IntentFilter f = new IntentFilter();
f.addAction(MessageCenterService.ACTION_PRESENCE);
f.addAction(MessageCenterService.ACTION_ROSTER_MATCH);
f.addAction(MessageCenterService.ACTION_PUBLICKEY);
f.addAction(MessageCenterService.ACTION_BLOCKLIST);
f.addAction(MessageCenterService.ACTION_LAST_ACTIVITY);
f.addAction(MessageCenterService.ACTION_CONNECTED);
lbm.registerReceiver(receiver, f);
// request current connection status
MessageCenterService.requestConnectionStatus(mContext);
// wait for the service to complete its job
synchronized (this) {
// wait for connection
try {
wait(MAX_WAIT_TIME);
}
catch (InterruptedException e) {
// simulate canceled operation
mCanceled = true;
}
}
lbm.unregisterReceiver(receiver);
// last chance to quit
if (mCanceled) throw new OperationCanceledException();
List<PresenceItem> res = receiver.getResponse();
if (res != null) {
ArrayList<ContentProviderOperation> operations =
new ArrayList<ContentProviderOperation>();
// TODO operations.size() could be used instead (?)
int op = 0;
// this is the time - delete all Kontalk raw contacts
try {
syncResult.stats.numDeletes += deleteAll(account, provider);
}
catch (Exception e) {
Log.e(TAG, "contact delete error", e);
syncResult.databaseError = true;
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
try {
syncResult.stats.numDeletes += deleteProfile(account, provider);
}
catch (Exception e) {
Log.e(TAG, "profile delete error", e);
syncResult.databaseError = true;
}
}
ContentValues registeredValues = new ContentValues();
registeredValues.put(Users.REGISTERED, 1);
for (int i = 0; i < res.size(); i++) {
PresenceItem entry = res.get(i);
if (entry.discarded)
continue;
final RawPhoneNumberEntry data = lookupNumbers
.get(XmppStringUtils.parseLocalpart(entry.from));
if (data != null && data.lookupKey != null) {
// add contact
addContact(account,
getDisplayName(provider, data.lookupKey, data.number),
data.number, data.jid, operations, op++);
}
else {
syncResult.stats.numSkippedEntries++;
}
// update fields
try {
String status = entry.status;
if (!TextUtils.isEmpty(status))
registeredValues.put(Users.STATUS, status);
else
registeredValues.putNull(Users.STATUS);
if (entry.timestamp >= 0)
registeredValues.put(Users.LAST_SEEN, entry.timestamp);
else
registeredValues.putNull(Users.LAST_SEEN);
if (entry.publicKey != null) {
try {
PGPPublicKey pubKey = PGP.getMasterKey(entry.publicKey);
// trust our own key blindly
int trustLevel = Authenticator.isSelfJID(mContext, entry.from) ?
MyUsers.Keys.TRUST_VERIFIED : -1;
// update keys table immediately
Keyring.setKey(mContext, entry.from, entry.publicKey, trustLevel);
// no data from system contacts, use name from public key
if (data == null) {
PGPUserID uid = PGP.parseUserId(pubKey, XmppStringUtils.parseDomain(entry.from));
if (uid != null) {
registeredValues.put(Users.DISPLAY_NAME, uid.getName());
}
}
}
catch (Exception e) {
Log.w(TAG, "unable to parse public key", e);
}
}
else {
// use roster name if no contact data available
if (data == null && entry.rosterName != null) {
registeredValues.put(Users.DISPLAY_NAME, entry.rosterName);
}
}
// blocked status
registeredValues.put(Users.BLOCKED, entry.blocked);
// user JID as reported by the server
registeredValues.put(Users.JID, entry.from);
/*
* Since UsersProvider.resync inserted the user row
* using our server name, it might have changed because
* of what the server reported. We already put into the
* values the new JID, but we need to use the old one
* in the where condition so we will have a match.
*/
String origJid;
if (data != null)
origJid = XMPPUtils.createLocalJID(mContext,
XmppStringUtils.parseLocalpart(entry.from));
else
origJid = entry.from;
usersProvider.update(Users.CONTENT_URI_OFFLINE, registeredValues,
Users.JID + " = ?", new String[] { origJid });
// clear data
registeredValues.remove(Users.DISPLAY_NAME);
// if this is our own contact, trust our own key later
if (Authenticator.isSelfJID(mContext, entry.from)) {
// register our profile while we're at it
if (data != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
// add contact
addProfile(account,
Authenticator.getDefaultDisplayName(mContext),
data.number, data.jid, operations, op++);
}
}
}
catch (Exception e) {
Log.e(TAG, "error updating users database", e);
// we shall continue here...
}
}
try {
if (operations.size() > 0)
provider.applyBatch(operations);
syncResult.stats.numInserts += op;
syncResult.stats.numEntries += op;
}
catch (Exception e) {
Log.w(TAG, "contact write error", e);
syncResult.stats.numSkippedEntries += op;
/*
* We do not consider system contacts failure a fatal error.
* This is actually a workaround for systems with disabled permissions or
* exotic firmwares. It can also protect against security 3rd party apps or
* non-Android platforms, such as Jolla/Alien Dalvik.
*/
}
commit(usersProvider, syncResult);
}
// timeout or error
else {
Log.w(TAG, "connection timeout - aborting sync");
syncResult.stats.numIoExceptions++;
}
}
}
private void commit(ContentProviderClient usersProvider, SyncResult syncResult) {
// commit users table
Uri uri = Users.CONTENT_URI.buildUpon()
.appendQueryParameter(Users.RESYNC, "true")
.appendQueryParameter(Users.COMMIT, "true")
.build();
try {
usersProvider.update(uri, null, null, null);
Log.d(TAG, "users database committed");
Contact.invalidate();
}
catch (Exception e) {
Log.e(TAG, "error committing users database - aborting sync", e);
syncResult.databaseError = true;
}
}
private void requestRosterMatch(String id, List<String> list) {
Intent i = new Intent(mContext, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_ROSTER_MATCH);
i.putExtra(MessageCenterService.EXTRA_PACKET_ID, id);
i.putExtra(MessageCenterService.EXTRA_JIDLIST, list.toArray(new String[list.size()]));
mContext.startService(i);
}
private void requestPresenceData(String id) {
Intent i = new Intent(mContext, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_PRESENCE);
i.putExtra(MessageCenterService.EXTRA_TYPE, Presence.Type.probe.toString());
i.putExtra(MessageCenterService.EXTRA_PACKET_ID, id);
mContext.startService(i);
}
private void requestPublicKeys() {
Intent i = new Intent(mContext, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_PUBLICKEY);
i.putExtra(MessageCenterService.EXTRA_PACKET_ID, IQ_PACKET_ID);
mContext.startService(i);
}
private void requestBlocklist() {
Intent i = new Intent(mContext, MessageCenterService.class);
i.setAction(MessageCenterService.ACTION_BLOCKLIST);
mContext.startService(i);
}
private String getDisplayName(ContentProviderClient client, String lookupKey, String defaultValue) {
String displayName = null;
Cursor nameQuery = null;
try {
nameQuery = client.query(
Uri.withAppendedPath(ContactsContract.Contacts
.CONTENT_LOOKUP_URI, lookupKey),
new String[] { ContactsContract.Contacts.DISPLAY_NAME },
null, null, null);
if (nameQuery.moveToFirst())
displayName = nameQuery.getString(0);
}
catch (Exception e) {
// ignored
}
finally {
// close cursor
try {
nameQuery.close();
}
catch (Exception ignored) {}
}
return (displayName != null) ? displayName : defaultValue;
}
private int deleteAll(Account account, ContentProviderClient provider)
throws RemoteException {
return provider.delete(RawContacts.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
.appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type)
.build(), null, null);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
private int deleteProfile(Account account, ContentProviderClient provider)
throws RemoteException {
return provider.delete(ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
.appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type)
.build(), null, null);
}
/*
private int deleteContact(Account account, long rawContactId) {
Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)
.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
.appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type)
.build();
ContentProviderClient client = mContext.getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
try {
return client.delete(uri, null, null);
}
catch (RemoteException e) {
Log.e(TAG, "delete error", e);
}
finally {
client.release();
}
return -1;
}
*/
private void addContact(Account account, String username, String phone, String jid,
List<ContentProviderOperation> operations, int index) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "adding contact \"" + username + "\" <" + phone + ">");
}
// create our RawContact
operations.add(insertRawContact(account, username, phone, jid,
RawContacts.CONTENT_URI).build());
// add contact data
addContactData(username, phone, operations, index);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
private void addProfile(Account account, String username, String phone, String jid,
List<ContentProviderOperation> operations, int index) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "adding profile \"" + username + "\" <" + phone + ">");
}
// create our RawContact
operations.add(insertRawContact(account, username, phone, jid,
ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI).build());
// add contact data
addContactData(username, phone, operations, index);
}
private ContentProviderOperation.Builder insertRawContact(Account account, String username, String phone, String jid, Uri uri) {
return ContentProviderOperation.newInsert(uri)
.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT)
.withValue(RawContacts.ACCOUNT_NAME, account.name)
.withValue(RawContacts.ACCOUNT_TYPE, account.type)
.withValue(RAW_COLUMN_DISPLAY_NAME, username)
.withValue(RAW_COLUMN_PHONE, phone)
.withValue(RAW_COLUMN_USERID, jid);
}
private void addContactData(String username, String phone, List<ContentProviderOperation> operations, int index) {
ContentProviderOperation.Builder builder;
final int opIndex = index * 3;
// create a Data record of common type 'StructuredName' for our RawContact
builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, opIndex)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, username);
operations.add(builder.build());
// create a Data record of custom type 'org.kontalk.user' to display a link to the conversation
builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, opIndex)
.withValue(ContactsContract.Data.MIMETYPE, Users.CONTENT_ITEM_TYPE)
.withValue(DATA_COLUMN_DISPLAY_NAME, username)
.withValue(DATA_COLUMN_ACCOUNT_NAME, mContext.getString(R.string.app_name))
.withValue(DATA_COLUMN_PHONE, phone)
.withYieldAllowed(true);
operations.add(builder.build());
}
}