/* * Copyright (c) 2010-2019 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.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 . */ package org.linphone.contacts; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentResolver; import android.content.ContentUris; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.RawContacts; import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; import org.linphone.LinphoneContext; import org.linphone.R; import org.linphone.core.tools.Log; class AndroidContact implements Serializable { String mAndroidId; private String mAndroidRawId; private boolean isAndroidRawIdLinphone; private transient ArrayList mChangesToCommit; private byte[] mTempPicture; AndroidContact() { mChangesToCommit = new ArrayList<>(); isAndroidRawIdLinphone = false; mTempPicture = null; } String getAndroidId() { return mAndroidId; } void setAndroidId(String id) { mAndroidId = id; } boolean isAndroidContact() { return mAndroidId != null; } private void addChangesToCommit(ContentProviderOperation operation) { Log.i("[Contact] Added operation " + operation); if (mChangesToCommit == null) { mChangesToCommit = new ArrayList<>(); } mChangesToCommit.add(operation); } void saveChangesCommited() { if (ContactsManager.getInstance().hasReadContactsAccess() && mChangesToCommit.size() > 0) { try { ContentResolver contentResolver = LinphoneContext.instance().getApplicationContext().getContentResolver(); ContentProviderResult[] results = contentResolver.applyBatch(ContactsContract.AUTHORITY, mChangesToCommit); if (results != null && results.length > 0 && results[0] != null && results[0].uri != null) { String rawId = String.valueOf(ContentUris.parseId(results[0].uri)); if (mAndroidId == null) { Log.i("[Contact] Contact created with RAW ID " + rawId); mAndroidRawId = rawId; if (mTempPicture != null) { Log.i( "[Contact] Contact has been created, raw is is available, time to set the photo"); setPhoto(mTempPicture); } final String[] projection = new String[] {ContactsContract.RawContacts.CONTACT_ID}; final Cursor cursor = contentResolver.query(results[0].uri, projection, null, null, null); if (cursor != null) { cursor.moveToNext(); long contactId = cursor.getLong(0); mAndroidId = String.valueOf(contactId); cursor.close(); Log.i("[Contact] Contact created with ID " + mAndroidId); } } else { if (mAndroidRawId == null || !isAndroidRawIdLinphone) { Log.i( "[Contact] Linphone RAW ID " + rawId + " created from existing RAW ID " + mAndroidRawId); mAndroidRawId = rawId; isAndroidRawIdLinphone = true; } } } } catch (Exception e) { Log.e("[Contact] Exception while saving changes: " + e); } finally { mChangesToCommit.clear(); } } } void createAndroidContact() { if (LinphoneContext.instance() .getApplicationContext() .getResources() .getBoolean(R.bool.use_linphone_tag)) { Log.i("[Contact] Creating contact using linphone account type"); addChangesToCommit( ContentProviderOperation.newInsert(RawContacts.CONTENT_URI) .withValue( RawContacts.ACCOUNT_TYPE, ContactsManager.getInstance() .getString(R.string.sync_account_type)) .withValue( RawContacts.ACCOUNT_NAME, ContactsManager.getInstance() .getString(R.string.sync_account_name)) .withValue( RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT) .build()); isAndroidRawIdLinphone = true; } else { Log.i("[Contact] Creating contact using default account type"); addChangesToCommit( ContentProviderOperation.newInsert(RawContacts.CONTENT_URI) .withValue(RawContacts.ACCOUNT_TYPE, null) .withValue(RawContacts.ACCOUNT_NAME, null) .withValue( RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT) .build()); } } void deleteAndroidContact() { Log.i("[Contact] Deleting Android contact ", this); ContactsManager.getInstance().delete(mAndroidId); } Uri getContactThumbnailPictureUri() { Uri person = ContentUris.withAppendedId(Contacts.CONTENT_URI, Long.parseLong(mAndroidId)); return Uri.withAppendedPath(person, Contacts.Photo.CONTENT_DIRECTORY); } Uri getContactPictureUri() { Uri person = ContentUris.withAppendedId(Contacts.CONTENT_URI, Long.parseLong(mAndroidId)); return Uri.withAppendedPath(person, Contacts.Photo.DISPLAY_PHOTO); } void setName(String fn, String ln) { if ((fn == null || fn.isEmpty()) && (ln == null || ln.isEmpty())) { Log.e("[Contact] Can't set both first and last name to null or empty"); return; } if (mAndroidId == null) { Log.i("[Contact] Setting given & family name " + fn + " " + ln + " to new contact."); addChangesToCommit( ContentProviderOperation.newInsert(Data.CONTENT_URI) .withValueBackReference(Data.RAW_CONTACT_ID, 0) .withValue( Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) .withValue(CommonDataKinds.StructuredName.GIVEN_NAME, fn) .withValue(CommonDataKinds.StructuredName.FAMILY_NAME, ln) .build()); } else { Log.i( "[Contact] Setting given & family name " + fn + " " + ln + " to existing contact " + mAndroidId + " (" + mAndroidRawId + ")"); String select = ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?"; String[] args = new String[] { getAndroidId(), ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE }; addChangesToCommit( ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) .withSelection(select, args) .withValue( ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName .CONTENT_ITEM_TYPE) .withValue( ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, fn) .withValue( ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, ln) .build()); } } void updateNativeContactWithPresenceInfo(String value) { Log.d("[Contact] Adding presence information " + value); addChangesToCommit( ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValue(ContactsContract.Data.RAW_CONTACT_ID, mAndroidRawId) .withValue( ContactsContract.Data.MIMETYPE, ContactsManager.getInstance() .getString(R.string.linphone_address_mime_type)) .withValue("data1", value) // phone number .withValue( "data2", ContactsManager.getInstance() .getString(R.string.app_name)) // Summary .withValue("data3", value) // Detail .build()); } boolean isLinphoneAddressMimeEntryAlreadyExisting(String value) { boolean result = false; ContentResolver resolver = LinphoneContext.instance().getApplicationContext().getContentResolver(); String[] projection = {"data1", "data3"}; String selection = ContactsContract.Data.RAW_CONTACT_ID + " = ? AND " + Data.MIMETYPE + " = ? AND data1 = ?"; Cursor c = resolver.query( ContactsContract.Data.CONTENT_URI, projection, selection, new String[] { mAndroidRawId, ContactsManager.getInstance() .getString(R.string.linphone_address_mime_type), value }, null); if (c != null) { if (c.moveToFirst()) { result = true; } c.close(); } return result; } void addNumberOrAddress(String value, String oldValueToReplace, boolean isSIP) { if (value == null || value.isEmpty()) { Log.e("[Contact] Can't add null or empty number or address"); return; } if (oldValueToReplace != null) { if (mAndroidId == null) { Log.e("[Contact] Can't update a number or address in non existing contact"); return; } Log.i( "[Contact] Updating " + oldValueToReplace + " by " + value + " in contact " + mAndroidId + " (" + mAndroidRawId + ")"); if (isSIP) { String select = ContactsContract.Data.CONTACT_ID + "=? AND (" + ContactsContract.Data.MIMETYPE + "=? OR " + ContactsContract.Data.MIMETYPE + "=? OR " + ContactsContract.Data.MIMETYPE + "=?) AND data1=?"; String[] args = new String[] { mAndroidId, "vnd.android.cursor.item/org.linphone.profile", // Old value ContactsManager.getInstance() .getString(R.string.linphone_address_mime_type), ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE, oldValueToReplace }; addChangesToCommit( ContentProviderOperation.newUpdate(Data.CONTENT_URI) .withSelection(select, args) .withValue( Data.MIMETYPE, ContactsManager.getInstance() .getString(R.string.linphone_address_mime_type)) .withValue("data1", value) // Value .withValue( "data2", ContactsManager.getInstance() .getString(R.string.app_name)) // Summary .withValue("data3", value) // Detail .build()); } else { String select = ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=? AND data1=?"; String[] args = new String[] { mAndroidId, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, oldValueToReplace }; addChangesToCommit( ContentProviderOperation.newUpdate(Data.CONTENT_URI) .withSelection(select, args) .withValue(Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, value) .withValue( ContactsContract.CommonDataKinds.Phone.TYPE, CommonDataKinds.Phone.TYPE_MOBILE) .build()); } } else { if (mAndroidId == null) { Log.i("[Contact] Adding number or address " + value + " to new contact."); if (isSIP) { addChangesToCommit( ContentProviderOperation.newInsert(Data.CONTENT_URI) .withValueBackReference(Data.RAW_CONTACT_ID, 0) .withValue( Data.MIMETYPE, ContactsManager.getInstance() .getString(R.string.linphone_address_mime_type)) .withValue("data1", value) // Value .withValue( "data2", ContactsManager.getInstance() .getString(R.string.app_name)) // Summary .withValue("data3", value) // Detail .build()); } else { addChangesToCommit( ContentProviderOperation.newInsert(Data.CONTENT_URI) .withValueBackReference(Data.RAW_CONTACT_ID, 0) .withValue( Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, value) .withValue( ContactsContract.CommonDataKinds.Phone.TYPE, CommonDataKinds.Phone.TYPE_MOBILE) .build()); } } else { Log.i( "[Contact] Adding number or address " + value + " to existing contact " + mAndroidId + " (" + mAndroidRawId + ")"); if (isSIP) { addChangesToCommit( ContentProviderOperation.newInsert(Data.CONTENT_URI) .withValue(ContactsContract.Data.RAW_CONTACT_ID, mAndroidRawId) .withValue( Data.MIMETYPE, ContactsManager.getInstance() .getString(R.string.linphone_address_mime_type)) .withValue("data1", value) // Value .withValue( "data2", ContactsManager.getInstance() .getString(R.string.app_name)) // Summary .withValue("data3", value) // Detail .build()); } else { addChangesToCommit( ContentProviderOperation.newInsert(Data.CONTENT_URI) .withValue(ContactsContract.Data.RAW_CONTACT_ID, mAndroidRawId) .withValue( Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, value) .withValue( ContactsContract.CommonDataKinds.Phone.TYPE, CommonDataKinds.Phone.TYPE_MOBILE) .build()); } } } } void removeNumberOrAddress(String noa, boolean isSIP) { if (noa == null || noa.isEmpty()) { Log.e("[Contact] Can't remove null or empty number or address."); return; } if (mAndroidId == null) { Log.e("[Contact] Can't remove a number or address from non existing contact"); } else { Log.i( "[Contact] Removing number or address " + noa + " from existing contact " + mAndroidId + " (" + mAndroidRawId + ")"); if (isSIP) { String select = ContactsContract.Data.CONTACT_ID + "=? AND (" + ContactsContract.Data.MIMETYPE + "=? OR " + ContactsContract.Data.MIMETYPE + "=?) AND data1=?"; String[] args = new String[] { mAndroidId, ContactsManager.getInstance() .getString(R.string.linphone_address_mime_type), ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE, noa }; addChangesToCommit( ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) .withSelection(select, args) .build()); } else { String select = ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=? AND data1=?"; String[] args = new String[] { mAndroidId, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, noa }; addChangesToCommit( ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) .withSelection(select, args) .build()); } } } void setOrganization(String org, String previousValue) { if (org == null || org.isEmpty()) { if (mAndroidId == null) { Log.e("[Contact] Can't set organization to null or empty for new contact"); return; } } if (mAndroidId == null) { Log.i("[Contact] Setting organization " + org + " to new contact."); addChangesToCommit( ContentProviderOperation.newInsert(Data.CONTENT_URI) .withValueBackReference(Data.RAW_CONTACT_ID, 0) .withValue( Data.MIMETYPE, CommonDataKinds.Organization.CONTENT_ITEM_TYPE) .withValue(CommonDataKinds.Organization.COMPANY, org) .build()); } else { if (previousValue != null) { String select = ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=? AND " + ContactsContract.CommonDataKinds.Organization.COMPANY + "=?"; String[] args = new String[] { getAndroidId(), ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE, previousValue }; Log.i( "[Contact] Updating organization " + org + " to existing contact " + mAndroidId + " (" + mAndroidRawId + ")"); addChangesToCommit( ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) .withSelection(select, args) .withValue( ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization .CONTENT_ITEM_TYPE) .withValue( ContactsContract.CommonDataKinds.Organization.COMPANY, org) .build()); } else { Log.i( "[Contact] Setting organization " + org + " to existing contact " + mAndroidId + " (" + mAndroidRawId + ")"); addChangesToCommit( ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValue(ContactsContract.Data.RAW_CONTACT_ID, mAndroidRawId) .withValue( ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization .CONTENT_ITEM_TYPE) .withValue( ContactsContract.CommonDataKinds.Organization.COMPANY, org) .build()); } } } void setPhoto(byte[] photo) { if (photo == null) { Log.e("[Contact] Can't set null picture."); return; } if (mAndroidRawId == null) { Log.w("[Contact] Can't set picture for not already created contact, will do it later"); mTempPicture = photo; } else { Log.i( "[Contact] Setting picture to an already created raw contact [", mAndroidRawId, "]"); try { long rawId = Long.parseLong(mAndroidRawId); Uri rawContactPhotoUri = Uri.withAppendedPath( ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawId), RawContacts.DisplayPhoto.CONTENT_DIRECTORY); if (rawContactPhotoUri != null) { ContentResolver resolver = LinphoneContext.instance().getApplicationContext().getContentResolver(); AssetFileDescriptor fd = resolver.openAssetFileDescriptor(rawContactPhotoUri, "rw"); OutputStream os = fd.createOutputStream(); os.write(photo); os.close(); fd.close(); } else { Log.e( "[Contact] Failed to get raw contact photo URI for raw contact id [", rawId, "], aborting"); } } catch (NumberFormatException nfe) { Log.e("[Contact] Couldn't parse raw id [", mAndroidId, "], aborting"); } catch (IOException ioe) { Log.e("[Contact] Couldn't set picture, IO error: ", ioe); } catch (Exception e) { Log.e("[Contact] Couldn't set picture, unknown error: ", e); } } } private String findRawContactID() { ContentResolver resolver = LinphoneContext.instance().getApplicationContext().getContentResolver(); String result = null; String[] projection = {ContactsContract.RawContacts._ID}; String selection = ContactsContract.RawContacts.CONTACT_ID + "=?"; Cursor c = resolver.query( ContactsContract.RawContacts.CONTENT_URI, projection, selection, new String[] {mAndroidId}, null); if (c != null) { if (c.moveToFirst()) { result = c.getString(c.getColumnIndex(ContactsContract.RawContacts._ID)); } c.close(); } return result; } void createRawLinphoneContactFromExistingAndroidContactIfNeeded() { if (LinphoneContext.instance() .getApplicationContext() .getResources() .getBoolean(R.bool.use_linphone_tag)) { if (mAndroidId != null && (mAndroidRawId == null || !isAndroidRawIdLinphone)) { if (mAndroidRawId == null) { Log.d("[Contact] RAW ID not found for contact " + mAndroidId); mAndroidRawId = findRawContactID(); } Log.d("[Contact] Found RAW ID for contact " + mAndroidId + " : " + mAndroidRawId); String linphoneRawId = findLinphoneRawContactId(); if (linphoneRawId == null) { Log.d("[Contact] Linphone RAW ID not found for contact " + mAndroidId); createRawLinphoneContactFromExistingAndroidContact(); } else { Log.d( "[Contact] Linphone RAW ID found for contact " + mAndroidId + " : " + linphoneRawId); mAndroidRawId = linphoneRawId; } isAndroidRawIdLinphone = true; } } } private void createRawLinphoneContactFromExistingAndroidContact() { addChangesToCommit( ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) .withValue( ContactsContract.RawContacts.ACCOUNT_TYPE, ContactsManager.getInstance().getString(R.string.sync_account_type)) .withValue( ContactsContract.RawContacts.ACCOUNT_NAME, ContactsManager.getInstance().getString(R.string.sync_account_name)) .withValue( ContactsContract.RawContacts.AGGREGATION_MODE, ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT) .build()); addChangesToCommit( ContentProviderOperation.newUpdate( ContactsContract.AggregationExceptions.CONTENT_URI) .withValue( ContactsContract.AggregationExceptions.TYPE, ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER) .withValue( ContactsContract.AggregationExceptions.RAW_CONTACT_ID1, mAndroidRawId) .withValueBackReference( ContactsContract.AggregationExceptions.RAW_CONTACT_ID2, 0) .build()); Log.i( "[Contact] Creating linphone RAW contact for contact " + mAndroidId + " linked with existing RAW contact " + mAndroidRawId); saveChangesCommited(); } private String findLinphoneRawContactId() { ContentResolver resolver = LinphoneContext.instance().getApplicationContext().getContentResolver(); String result = null; String[] projection = {ContactsContract.RawContacts._ID}; String selection = ContactsContract.RawContacts.CONTACT_ID + "=? AND " + ContactsContract.RawContacts.ACCOUNT_TYPE + "=?"; Cursor c = resolver.query( ContactsContract.RawContacts.CONTENT_URI, projection, selection, new String[] { mAndroidId, ContactsManager.getInstance().getString(R.string.sync_account_type) }, null); if (c != null) { if (c.moveToFirst()) { result = c.getString(c.getColumnIndex(ContactsContract.RawContacts._ID)); } c.close(); } return result; } }