/* * 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 static android.os.AsyncTask.THREAD_POOL_EXECUTOR; import android.Manifest; import android.accounts.Account; import android.accounts.AccountManager; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.pm.PackageManager; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.provider.ContactsContract; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import org.linphone.LinphoneContext; import org.linphone.LinphoneManager; import org.linphone.R; import org.linphone.compatibility.Compatibility; import org.linphone.core.Address; import org.linphone.core.Core; import org.linphone.core.Friend; import org.linphone.core.FriendList; import org.linphone.core.FriendListListener; import org.linphone.core.MagicSearch; import org.linphone.core.PresenceBasicStatus; import org.linphone.core.PresenceModel; import org.linphone.core.ProxyConfig; import org.linphone.core.tools.Log; import org.linphone.settings.LinphonePreferences; public class ContactsManager extends ContentObserver implements FriendListListener, LinphoneContext.CoreStartedListener { private List mContacts, mSipContacts; private final ArrayList mContactsUpdatedListeners; private MagicSearch mMagicSearch; private boolean mContactsFetchedOnce = false; private Context mContext; private AsyncContactsLoader mLoadContactTask; private boolean mInitialized = false; public static ContactsManager getInstance() { return LinphoneContext.instance().getContactsManager(); } public ContactsManager(Context context) { super(new Handler(Looper.getMainLooper())); mContext = context; mContactsUpdatedListeners = new ArrayList<>(); mContacts = new ArrayList<>(); mSipContacts = new ArrayList<>(); if (LinphoneManager.getCore() != null) { mMagicSearch = LinphoneManager.getCore().createMagicSearch(); mMagicSearch.setLimitedSearch(false); // Do not limit the number of results } LinphoneContext.instance().addCoreStartedListener(this); } public void addContactsListener(ContactsUpdatedListener listener) { mContactsUpdatedListeners.add(listener); } public void removeContactsListener(ContactsUpdatedListener listener) { mContactsUpdatedListeners.remove(listener); } public ArrayList getContactsListeners() { return mContactsUpdatedListeners; } @Override public void onChange(boolean selfChange) { onChange(selfChange, null); } @Override public void onChange(boolean selfChange, Uri uri) { Log.i("[Contacts Manager] Content observer detected a changing in at least one contact"); fetchContactsAsync(); } @Override public void onCoreStarted() { // Core has been started, fetch contacts again in case there are some // in the configuration file or remote provisioning fetchContactsAsync(); } public synchronized List getContacts() { return mContacts; } synchronized void setContacts(List c) { mContacts = c; } public synchronized List getSIPContacts() { return mSipContacts; } synchronized void setSipContacts(List c) { mSipContacts = c; } public void destroy() { mContext.getContentResolver().unregisterContentObserver(this); LinphoneContext.instance().removeCoreStartedListener(this); if (mLoadContactTask != null) { mLoadContactTask.cancel(true); } // LinphoneContact has a Friend field and Friend can have a LinphoneContact has userData // Friend also keeps a ref on the Core, so we have to clean them for (LinphoneContact c : mContacts) { c.setFriend(null); } mContacts.clear(); for (LinphoneContact c : mSipContacts) { c.setFriend(null); } mSipContacts.clear(); Core core = LinphoneManager.getCore(); if (core != null) { for (FriendList list : core.getFriendsLists()) { list.removeListener(this); } } } public void fetchContactsAsync() { if (mLoadContactTask != null) { mLoadContactTask.cancel(true); } if (!hasReadContactsAccess()) { Log.w( "[Contacts Manager] Can't fetch native contacts without READ_CONTACTS permission"); } mLoadContactTask = new AsyncContactsLoader(mContext); mContactsFetchedOnce = true; mLoadContactTask.executeOnExecutor(THREAD_POOL_EXECUTOR); } public MagicSearch getMagicSearch() { return mMagicSearch; } public boolean contactsFetchedOnce() { return mContactsFetchedOnce; } public List getContacts(String search) { search = search.toLowerCase(Locale.getDefault()); List searchContactsBegin = new ArrayList<>(); List searchContactsContain = new ArrayList<>(); for (LinphoneContact contact : getContacts()) { if (contact.getFullName() != null) { if (contact.getFullName().toLowerCase(Locale.getDefault()).startsWith(search)) { searchContactsBegin.add(contact); } else if (contact.getFullName() .toLowerCase(Locale.getDefault()) .contains(search)) { searchContactsContain.add(contact); } } } searchContactsBegin.addAll(searchContactsContain); return searchContactsBegin; } public List getSIPContacts(String search) { search = search.toLowerCase(Locale.getDefault()); List searchContactsBegin = new ArrayList<>(); List searchContactsContain = new ArrayList<>(); for (LinphoneContact contact : getSIPContacts()) { if (contact.getFullName() != null) { if (contact.getFullName().toLowerCase(Locale.getDefault()).startsWith(search)) { searchContactsBegin.add(contact); } else if (contact.getFullName() .toLowerCase(Locale.getDefault()) .contains(search)) { searchContactsContain.add(contact); } } } searchContactsBegin.addAll(searchContactsContain); return searchContactsBegin; } public void enableContactsAccess() { LinphonePreferences.instance().disableFriendsStorage(); } public boolean hasReadContactsAccess() { if (mContext == null || mContext.getPackageManager() == null) { return false; } boolean contactsR = (PackageManager.PERMISSION_GRANTED == mContext.getPackageManager() .checkPermission( android.Manifest.permission.READ_CONTACTS, mContext.getPackageName())); return contactsR && !mContext.getResources().getBoolean(R.bool.force_use_of_linphone_friends); } private boolean hasWriteContactsAccess() { if (mContext == null) { return false; } return (PackageManager.PERMISSION_GRANTED == mContext.getPackageManager() .checkPermission( Manifest.permission.WRITE_CONTACTS, mContext.getPackageName())); } private boolean hasWriteSyncPermission() { if (mContext == null) { return false; } return (PackageManager.PERMISSION_GRANTED == mContext.getPackageManager() .checkPermission( Manifest.permission.WRITE_SYNC_SETTINGS, mContext.getPackageName())); } public boolean isLinphoneContactsPrefered() { ProxyConfig lpc = LinphoneManager.getCore().getDefaultProxyConfig(); return lpc != null && lpc.getIdentityAddress() .getDomain() .equals(mContext.getString(R.string.default_domain)); } public void initializeContactManager() { if (!mInitialized) { if (mContext.getResources().getBoolean(R.bool.use_linphone_tag)) { if (hasReadContactsAccess() && hasWriteContactsAccess() && hasWriteSyncPermission()) { if (LinphoneContext.isReady()) { initializeSyncAccount(); mInitialized = true; } } } } } private void makeContactAccountVisible() { ContentProviderClient client = mContext.getContentResolver() .acquireContentProviderClient(ContactsContract.AUTHORITY_URI); if (client == null) { Log.e( "[Contacts Manager] Failed to get content provider client for contacts authority!"); return; } ContentValues values = new ContentValues(); values.put( ContactsContract.Settings.ACCOUNT_NAME, mContext.getString(R.string.sync_account_name)); values.put( ContactsContract.Settings.ACCOUNT_TYPE, mContext.getString(R.string.sync_account_type)); values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, true); try { client.insert( ContactsContract.Settings.CONTENT_URI .buildUpon() .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") .build(), values); Log.i("[Contacts Manager] Contacts account made visible"); } catch (RemoteException e) { Log.e("[Contacts Manager] Couldn't make contacts account visible: " + e); } Compatibility.closeContentProviderClient(client); } private void initializeSyncAccount() { AccountManager accountManager = (AccountManager) mContext.getSystemService(Context.ACCOUNT_SERVICE); Account[] accounts = accountManager.getAccountsByType(mContext.getString(R.string.sync_account_type)); if (accounts != null && accounts.length == 0) { Account newAccount = new Account( mContext.getString(R.string.sync_account_name), mContext.getString(R.string.sync_account_type)); try { accountManager.addAccountExplicitly(newAccount, null, null); Log.i("[Contacts Manager] Contact account added"); makeContactAccountVisible(); } catch (Exception e) { Log.e("[Contacts Manager] Couldn't initialize sync account: " + e); } } else if (accounts != null) { for (Account account : accounts) { Log.i( "[Contacts Manager] Found account with name \"" + account.name + "\" and type \"" + account.type + "\""); makeContactAccountVisible(); } } } public String getAndroidContactIdFromUri(Uri uri) { String[] projection = {ContactsContract.CommonDataKinds.SipAddress.CONTACT_ID}; Cursor cursor = mContext.getApplicationContext() .getContentResolver() .query(uri, projection, null, null, null); cursor.moveToFirst(); int nameColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.SipAddress.CONTACT_ID); String id = cursor.getString(nameColumnIndex); cursor.close(); return id; } public synchronized LinphoneContact findContactFromAndroidId(String androidId) { if (androidId == null) { return null; } for (LinphoneContact c : getContacts()) { if (c.getAndroidId() != null && c.getAndroidId().equals(androidId)) { return c; } } return null; } public synchronized LinphoneContact findContactFromAddress(Address address) { if (address == null) return null; Core core = LinphoneManager.getCore(); Friend lf = core.findFriend(address); if (lf != null) { return (LinphoneContact) lf.getUserData(); } String username = address.getUsername(); if (username == null) { Log.w("[Contacts Manager] Address ", address.asString(), " doesn't have a username!"); return null; } if (android.util.Patterns.PHONE.matcher(username).matches()) { return findContactFromPhoneNumber(username); } return null; } public synchronized LinphoneContact findContactFromPhoneNumber(String phoneNumber) { if (phoneNumber == null) return null; if (!android.util.Patterns.PHONE.matcher(phoneNumber).matches()) { Log.w( "[Contacts Manager] Expected phone number but doesn't look like it: " + phoneNumber); return null; } Core core = LinphoneManager.getCore(); ProxyConfig lpc = null; if (core != null) { lpc = core.getDefaultProxyConfig(); } if (lpc == null) { Log.i("[Contacts Manager] Couldn't find default proxy config..."); return null; } String normalized = lpc.normalizePhoneNumber(phoneNumber); if (normalized == null) { Log.w( "[Contacts Manager] Couldn't normalize phone number " + phoneNumber + ", default proxy config prefix is " + lpc.getDialPrefix()); normalized = phoneNumber; } Address addr = lpc.normalizeSipUri(normalized); if (addr == null) { Log.w("[Contacts Manager] Couldn't normalize SIP URI " + normalized); return null; } // Without this, the hashmap inside liblinphone won't find it... addr.setUriParam("user", "phone"); Friend lf = core.findFriend(addr); if (lf != null) { return (LinphoneContact) lf.getUserData(); } Log.w("[Contacts Manager] Couldn't find friend..."); return null; } public String getAddressOrNumberForAndroidContact(ContentResolver resolver, Uri contactUri) { if (resolver == null || contactUri == null) return null; // Phone Numbers String[] projection = new String[] {ContactsContract.CommonDataKinds.Phone.NUMBER}; Cursor c = resolver.query(contactUri, projection, null, null, null); if (c != null) { if (c.moveToNext()) { int numberIndex = c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER); String number = c.getString(numberIndex); c.close(); return number; } c.close(); } projection = new String[] {ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS}; c = resolver.query(contactUri, projection, null, null, null); if (c != null) { if (c.moveToNext()) { int numberIndex = c.getColumnIndex(ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS); String address = c.getString(numberIndex); c.close(); return address; } c.close(); } return null; } private synchronized boolean refreshSipContact(Friend lf) { if (lf == null) return false; LinphoneContact contact = (LinphoneContact) lf.getUserData(); if (contact != null) { if (LinphoneContext.instance() .getApplicationContext() .getResources() .getBoolean(R.bool.use_linphone_tag)) { if (LinphonePreferences.instance() .isPresenceStorageInNativeAndroidContactEnabled()) { // Inserting information in Android contact if the parameter is enabled for (LinphoneNumberOrAddress noa : contact.getNumbersOrAddresses()) { if (noa.isSIPAddress()) { // We are only interested in phone numbers continue; } String value = noa.getValue(); if (value == null || value.isEmpty()) { continue; } // Test presence of the value PresenceModel pm = contact.getFriend().getPresenceModelForUriOrTel(value); // If presence is not null if (pm != null && pm.getBasicStatus() != null && pm.getBasicStatus().equals(PresenceBasicStatus.Open)) { // Add presence to native contact AsyncContactPresence asyncContactPresence = new AsyncContactPresence(contact, value); asyncContactPresence.execute(); } } } } if (!mSipContacts.contains(contact)) { mSipContacts.add(contact); return true; } } return false; } public void delete(String id) { ArrayList ids = new ArrayList<>(); ids.add(id); deleteMultipleContactsAtOnce(ids); } public void deleteMultipleContactsAtOnce(List ids) { String select = ContactsContract.Data.CONTACT_ID + " = ?"; ArrayList ops = new ArrayList<>(); for (String id : ids) { Log.i("[Contacts Manager] Adding Android contact id ", id, " to batch removal"); String[] args = new String[] {id}; ops.add( ContentProviderOperation.newDelete(ContactsContract.RawContacts.CONTENT_URI) .withSelection(select, args) .build()); } try { mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops); } catch (Exception e) { Log.e("[Contacts Manager] " + e); } // To ensure removed contacts won't appear in the contacts list anymore fetchContactsAsync(); } public String getString(int resourceID) { if (mContext == null) return null; return mContext.getString(resourceID); } @Override public void onContactCreated(FriendList list, Friend lf) {} @Override public void onContactDeleted(FriendList list, Friend lf) {} @Override public void onContactUpdated(FriendList list, Friend newFriend, Friend oldFriend) {} @Override public void onSyncStatusChanged(FriendList list, FriendList.SyncStatus status, String msg) {} @Override public void onPresenceReceived(FriendList list, Friend[] friends) { boolean updated = false; for (Friend lf : friends) { boolean newContact = refreshSipContact(lf); if (newContact) { updated = true; } } if (updated) { Collections.sort(mSipContacts); } for (ContactsUpdatedListener listener : mContactsUpdatedListeners) { listener.onContactsUpdated(); } Compatibility.createChatShortcuts(mContext); } }