Skip to content
Snippets Groups Projects
Select Git revision
  • 1403e8dd7d63f8aabf20288f76ab6926ff30d1a3
  • master default protected
  • nightly/20250722.0
  • beta/202507211539
  • stable/20250718.0
  • nightly/20250718.0
  • nightly/20250714.0
  • beta/202507141552
  • beta/202506161038
  • stable/20250613.0
  • nightly/20250613.0
  • beta/202506101658
  • stable/20250610.0
  • nightly/20250610.0
  • beta/202506091027
  • beta/202506061543
  • nightly/20250605.0
  • beta/202506051039
  • beta/202506051002
  • beta/202506041611
  • beta/202506041335
  • beta/202505231812
22 results

currentconversation.cpp

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    LocalService.java 63.03 KiB
    /*
     *  Copyright (C) 2015-2016 Savoir-faire Linux Inc.
     *
     *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
     *
     *  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 cx.ring.service;
    
    import android.Manifest;
    import android.app.PendingIntent;
    import android.app.Service;
    import android.content.AsyncTaskLoader;
    import android.content.BroadcastReceiver;
    import android.content.ComponentName;
    import android.content.ContentResolver;
    import android.content.ContentUris;
    import android.content.Context;
    import android.content.Intent;
    import android.content.IntentFilter;
    import android.content.Loader;
    import android.content.ServiceConnection;
    import android.content.SharedPreferences;
    import android.content.pm.PackageManager;
    import android.content.res.Resources;
    import android.database.ContentObserver;
    import android.database.Cursor;
    import android.graphics.Bitmap;
    import android.media.AudioManager;
    import android.net.ConnectivityManager;
    import android.net.NetworkInfo;
    import android.net.Uri;
    import android.os.AsyncTask;
    import android.os.Binder;
    import android.os.IBinder;
    import android.os.OperationCanceledException;
    import android.os.RemoteException;
    import android.preference.PreferenceManager;
    import android.provider.ContactsContract;
    import android.provider.ContactsContract.CommonDataKinds.Im;
    import android.provider.ContactsContract.CommonDataKinds.Phone;
    import android.provider.ContactsContract.CommonDataKinds.SipAddress;
    import android.support.annotation.NonNull;
    import android.support.v4.app.NotificationCompat;
    import android.support.v4.app.NotificationManagerCompat;
    import android.support.v4.content.ContextCompat;
    import android.text.Html;
    import android.text.format.DateUtils;
    import android.util.Log;
    import android.util.LongSparseArray;
    import android.util.LruCache;
    import android.util.Pair;
    
    import java.io.File;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.NavigableMap;
    import java.util.Random;
    import java.util.TreeMap;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    import cx.ring.BuildConfig;
    import cx.ring.R;
    import cx.ring.client.ConversationActivity;
    import cx.ring.history.HistoryCall;
    import cx.ring.history.HistoryEntry;
    import cx.ring.history.HistoryManager;
    import cx.ring.history.HistoryText;
    import cx.ring.loaders.AccountsLoader;
    import cx.ring.loaders.ContactsLoader;
    import cx.ring.model.CallContact;
    import cx.ring.model.Conference;
    import cx.ring.model.Conversation;
    import cx.ring.model.SecureSipCall;
    import cx.ring.model.SipCall;
    import cx.ring.model.SipUri;
    import cx.ring.model.TextMessage;
    import cx.ring.model.account.Account;
    import cx.ring.model.account.AccountDetailAdvanced;
    import cx.ring.model.account.AccountDetailSrtp;
    import cx.ring.model.account.AccountDetailTls;
    import cx.ring.utils.MediaManager;
    
    public class LocalService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener
    {
        static final String TAG = LocalService.class.getSimpleName();
    
        // Emitting events
        static public final String ACTION_CONF_UPDATE = BuildConfig.APPLICATION_ID + ".action.CONF_UPDATE";
        static public final String ACTION_CONF_LOADED = BuildConfig.APPLICATION_ID + ".action.CONF_LOADED";
        static public final String ACTION_ACCOUNT_UPDATE = BuildConfig.APPLICATION_ID + ".action.ACCOUNT_UPDATE";
        static public final String ACTION_CONV_READ = BuildConfig.APPLICATION_ID + ".action.CONV_READ";
    
        static public final String ACTION_CONF_UPDATE_EXTRA_MSG = ACTION_CONF_UPDATE + ".extra.message";
    
        // Receiving commands
        static public final String ACTION_CALL_ACCEPT = BuildConfig.APPLICATION_ID + ".action.CALL_ACCEPT";
        static public final String ACTION_CALL_REFUSE = BuildConfig.APPLICATION_ID + ".action.CALL_REFUSE";
        static public final String ACTION_CALL_END = BuildConfig.APPLICATION_ID + ".action.CALL_END";
    
    
        public static final Uri AUTHORITY_URI = Uri.parse("content://" + BuildConfig.APPLICATION_ID);
        public static final int PERMISSIONS_REQUEST = 57;
    
        public final static String[] REQUIRED_RUNTIME_PERMISSIONS = {Manifest.permission.RECORD_AUDIO};
    
        private IDRingService mService = null;
        private boolean dringStarted = false;
    
        private final ContactsContentObserver contactContentObserver = new ContactsContentObserver();
    
        // Binder given to clients
        private final IBinder mBinder = new LocalBinder();
    
        private Map<String, Conversation> conversations = new HashMap<>();
        private LongSparseArray<TextMessage> messages = new LongSparseArray<>();
    
        private List<Account> accounts = new ArrayList<>();
    
        private HistoryManager historyManager;
    
        private final LongSparseArray<CallContact> systemContactCache = new LongSparseArray<>();
        private ContactsLoader.Result lastContactLoaderResult = new ContactsLoader.Result();
    
        private ContactsLoader mSystemContactLoader = null;
        private AccountsLoader mAccountLoader = null;
    
        private LruCache<Long, Bitmap> mMemoryCache = null;
        private final ExecutorService mPool = Executors.newCachedThreadPool();
    
        private NotificationManagerCompat notificationManager;
        private MediaManager mediaManager;
    
        private boolean isWifiConn = false;
        private boolean isMobileConn = false;
    
        private boolean canUseContacts = true;
        private boolean canUseMobile = false;
    
        private boolean mAreConversationsLoaded = false;
    
        public ContactsLoader.Result getSortedContacts() {
            Log.w(TAG, "getSortedContacts " + lastContactLoaderResult.contacts.size() + " contacts, " + lastContactLoaderResult.starred.size() + " starred.");
            return lastContactLoaderResult;
        }
    
        public LruCache<Long, Bitmap> get40dpContactCache() {
            return mMemoryCache;
        }
    
        public ExecutorService getThreadPool() {
            return mPool;
        }
    
        public LongSparseArray<CallContact> getContactCache() {
            return systemContactCache;
        }
    
        public boolean isConnected() {
            return isWifiConn || (canUseMobile && isMobileConn);
        }
        public boolean isWifiConnected() {
            return isWifiConn;
        }
    
        public Conference placeCall(SipCall call) {
            Conference conf = null;
            CallContact contact = call.getContact();
            if (contact == null)
                contact = findContactByNumber(call.getNumberUri());
            Conversation conv = startConversation(contact);
            try {
                mService.setPreviewSettings();
                SipUri number = call.getNumberUri();
                if (number == null || number.isEmpty())
                    number = contact.getPhones().get(0).getNumber();
                String callId = mService.placeCall(call.getAccount(), number.getUriString(), !call.isVideoMuted());
                if (callId == null || callId.isEmpty()) {
                    return null;
                }
                call.setCallID(callId);
                Account acc = getAccount(call.getAccount());
                if(acc.isRing()
                        || acc.getSrtpDetails().getDetailBoolean(AccountDetailSrtp.CONFIG_SRTP_ENABLE)
                        || acc.getTlsDetails().getDetailBoolean(AccountDetailTls.CONFIG_TLS_ENABLE)) {
                    Log.i(TAG, "placeCall() call is secure");
                    SecureSipCall secureCall = new SecureSipCall(call, acc.getSrtpDetails().getDetailString(AccountDetailSrtp.CONFIG_SRTP_KEY_EXCHANGE));
                    conf = new Conference(secureCall);
                } else {
                    conf = new Conference(call);
                }
                conf.getParticipants().get(0).setContact(contact);
                conv.addConference(conf);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            return conf;
        }
    
        public void sendTextMessage(String account, SipUri to, String txt) {
            try {
                long id = mService.sendAccountTextMessage(account, to.getRawUriString(), txt);
                Log.w(TAG, "sendAccountTextMessage " + txt + " got id " + id);
                TextMessage message = new TextMessage(false, txt, to, null, account);
                message.setID(id);
                message.read();
                historyManager.insertNewTextMessage(message);
                messages.put(id, message);
                textMessageSent(message);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    
        public void sendTextMessage(Conference conf, String txt) {
            try {
                mService.sendTextMessage(conf.getId(), txt);
                SipCall call = conf.getParticipants().get(0);
                TextMessage message = new TextMessage(false, txt, call.getNumberUri(), conf.getId(), call.getAccount());
                message.read();
                historyManager.insertNewTextMessage(message);
                textMessageSent(message);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    
        private void readTextMessage(TextMessage message) {
            message.read();
            HistoryText ht = new HistoryText(message);
            historyManager.updateTextMessage(ht);
        }
    
        public void readConversation(Conversation conv) {
            for (HistoryEntry h : conv.getRawHistory().values()) {
                NavigableMap<Long, TextMessage> messages = h.getTextMessages();
                for (TextMessage msg : messages.descendingMap().values()) {
                    if (msg.isRead())
                        break;
                    readTextMessage(msg);
                }
            }
            notificationManager.cancel(conv.notificationId);
            updateTextNotifications();
        }
    
        private Conversation conversationFromMessage(TextMessage txt) {
            Conversation conv;
            String call = txt.getCallId();
            if (call != null && !call.isEmpty()) {
                conv = getConversationByCallId(call);
            } else {
                conv = startConversation(findContactByNumber(txt.getNumberUri()));
                txt.setContact(conv.getContact());
            }
            return conv;
        }
    
        private void textMessageSent(TextMessage txt) {
            Log.d(TAG, "Sent text messsage " + txt.getAccount() + " " + txt.getCallId() + " " + txt.getNumberUri() + " " + txt.getMessage());
            Conversation conv = conversationFromMessage(txt);
            conv.addTextMessage(txt);
            if (conv.mVisible)
                txt.read();
            else
                updateTextNotifications();
            sendBroadcast(new Intent(ACTION_CONF_UPDATE));
        }
    
        public void refreshConversations() {
            Log.d(TAG, "refreshConversations()");
            new ConversationLoader(getApplicationContext().getContentResolver(), systemContactCache){
                @Override
                protected void onPostExecute(Map<String, Conversation> res) {
                    updated(res);
                }
            }.execute();
        }
    
        public interface Callbacks {
            IDRingService getRemoteService();
            LocalService getService();
        }
        public static class DummyCallbacks implements Callbacks {
            @Override
            public IDRingService getRemoteService() {
                return null;
            }
            @Override
            public LocalService getService() {
                return null;
            }
        }
        public static final Callbacks DUMMY_CALLBACKS = new DummyCallbacks();
    
        @Override
        public void onCreate() {
            Log.d(TAG, "onCreate");
            super.onCreate();
    
            mediaManager = new MediaManager(this);
    
            notificationManager = NotificationManagerCompat.from(this);
    
            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
            final int cacheSize = maxMemory / 8;
            mMemoryCache = new LruCache<Long, Bitmap>(cacheSize){
                @Override
                protected int sizeOf(Long key, Bitmap bitmap) {
                    return bitmap.getByteCount() / 1024;
                }
            };
    
            historyManager = new HistoryManager(this);
            startListener();
            Intent intent = new Intent(this, DRingService.class);
            startService(intent);
            bindService(intent, mConnection, BIND_AUTO_CREATE | BIND_IMPORTANT | BIND_ABOVE_CLIENT);
    
            ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo ni = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
            isWifiConn = ni != null && ni.isConnected();
            ni = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
            isMobileConn = ni != null && ni.isConnected();
    
            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
            canUseContacts = sharedPreferences.getBoolean(getString(R.string.pref_systemContacts_key), true);
            canUseMobile = sharedPreferences.getBoolean(getString(R.string.pref_mobileData_key), false);
            sharedPreferences.registerOnSharedPreferenceChangeListener(this);
        }
    
        @Override
        public void onLowMemory() {
            super.onLowMemory();
            mMemoryCache.evictAll();
        }
    
        @Override
        public void onDestroy() {
            Log.e(TAG, "onDestroy");
            super.onDestroy();
            PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this);
            stopListener();
            mMemoryCache.evictAll();
            mPool.shutdown();
            systemContactCache.clear();
            lastContactLoaderResult = null;
            mAccountLoader.abandon();
            mAccountLoader = null;
        }
    
        private final Loader.OnLoadCompleteListener<ArrayList<Account>> onAccountsLoaded = new Loader.OnLoadCompleteListener<ArrayList<Account>>() {
            @Override
            public void onLoadComplete(Loader<ArrayList<Account>> loader, ArrayList<Account> data) {
                Log.w(TAG, "AccountsLoader Loader.OnLoadCompleteListener " + data.size());
                accounts = data;
                mAccountLoader.stopLoading();
                boolean haveSipAccount = false;
                boolean haveRingAccount = false;
                for (Account acc : accounts) {
                    //~ Sipinfo is forced for any sipaccount since overrtp is not supported yet.
                    //~ This will have to be removed when it will be supported.
                    Log.d(TAG, "Settings SIP DTMF type to sipinfo");
                    acc.getAdvancedDetails().setDetailString(
                            AccountDetailAdvanced.CONFIG_ACCOUNT_DTMF_TYPE,
                            getString(R.string.account_sip_dtmf_type_sipinfo)
                    );
    
                    try {
                        final IDRingService remote = getRemoteService();
                        remote.setAccountDetails(acc.getAccountID(),acc.getDetails());
                    }
                    catch (android.os.RemoteException exception) {
                        exception.printStackTrace();
                    }
    
                    if (!acc.isEnabled())
                        continue;
                    if (acc.isSip()) {
                        haveSipAccount = true;
                    }
                    else if (acc.isRing())
                        haveRingAccount = true;
                }
    
                mSystemContactLoader.loadRingContacts = haveRingAccount;
                mSystemContactLoader.loadSipContacts = haveSipAccount;
    
                SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(LocalService.this);
                sharedPreferences.edit()
                        .putBoolean(OutgoingCallHandler.KEY_CACHE_HAVE_RINGACCOUNT, haveRingAccount)
                        .putBoolean(OutgoingCallHandler.KEY_CACHE_HAVE_SIPACCOUNT, haveSipAccount).apply();
    
                updateConnectivityState();
                mSystemContactLoader.startLoading();
                mSystemContactLoader.forceLoad();
            }
        };
        private final Loader.OnLoadCompleteListener<ContactsLoader.Result> onSystemContactsLoaded = new Loader.OnLoadCompleteListener<ContactsLoader.Result>() {
            @Override
            public void onLoadComplete(Loader<ContactsLoader.Result> loader, ContactsLoader.Result data) {
                Log.w(TAG, "ContactsLoader Loader.OnLoadCompleteListener " + data.contacts.size() + " contacts, " + data.starred.size() + " starred.");
    
                lastContactLoaderResult = data;
                systemContactCache.clear();
                for (CallContact c : data.contacts)
                    systemContactCache.put(c.getId(), c);
    
                refreshConversations();
            }
        };
    
        @Override
        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
            if (key.equals(getString(R.string.pref_systemContacts_key))) {
                canUseContacts = sharedPreferences.getBoolean(key, true);
                mSystemContactLoader.onContentChanged();
                mSystemContactLoader.startLoading();
            } else if (key.equals(getString(R.string.pref_mobileData_key))) {
                canUseMobile = sharedPreferences.getBoolean(key, true);
                updateConnectivityState();
            }
        }
    
        private ServiceConnection mConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName className, IBinder service) {
                Log.w(TAG, "onServiceConnected " + className.getClassName());
                mService = IDRingService.Stub.asInterface(service);
                mAccountLoader = new AccountsLoader(LocalService.this, mService);
                mAccountLoader.registerListener(1, onAccountsLoaded);
                try {
                    if (mService.isStarted()) {
                        mAccountLoader.startLoading();
                        mAccountLoader.onContentChanged();
                    }
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
    
                mSystemContactLoader = new ContactsLoader(LocalService.this);
                mSystemContactLoader.registerListener(1, onSystemContactsLoaded);
            }
    
            @Override
            public void onServiceDisconnected(ComponentName arg0) {
                Log.w(TAG, "onServiceDisconnected " + arg0.getClassName());
                if (mAccountLoader != null) {
                    mAccountLoader.unregisterListener(onAccountsLoaded);
                    mAccountLoader.cancelLoad();
                    mAccountLoader.stopLoading();
                    mAccountLoader = null;
                }
                if (mSystemContactLoader != null) {
                    mSystemContactLoader.unregisterListener(onSystemContactsLoaded);
                    mSystemContactLoader.cancelLoad();
                    mSystemContactLoader.stopLoading();
                    mSystemContactLoader = null;
                }
    
                mService = null;
            }
        };
    
        /**
         * Class used for the client Binder.  Because we know this service always
         * runs in the same process as its clients, we don't need to deal with IPC.
         */
        public class LocalBinder extends Binder {
            public LocalService getService() {
                // Return this instance of LocalService so clients can call public methods
                return LocalService.this;
            }
        }
    
        @Override
        public IBinder onBind(Intent intent) {
            return mBinder;
        }
    
        @Override
        public boolean onUnbind(Intent intent) {
            Log.e(TAG, "onUnbind");
            if (mConnection != null) {
                unbindService(mConnection);
                mConnection = null;
            }
            return super.onUnbind(intent);
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            if (intent != null && intent.getAction() != null && mService != null)
                receiver.onReceive(this, intent);
            return super.onStartCommand(intent, flags, startId);
        }
    
        public static boolean checkPermission(Context c, String permission) {
            return ContextCompat.checkSelfPermission(c, permission) == PackageManager.PERMISSION_GRANTED;
        }
    
        @NonNull
        public static String[] checkRequiredPermissions(Context c) {
            ArrayList<String> perms = new ArrayList<>();
            for (String p : REQUIRED_RUNTIME_PERMISSIONS) {
                if (!checkPermission(c, p))
                    perms.add(p);
            }
            SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(c);
            boolean contact_perm = sharedPref.getBoolean(c.getString(R.string.pref_systemContacts_key), true);
            if (contact_perm && !checkPermission(c, Manifest.permission.READ_CONTACTS))
                perms.add(Manifest.permission.READ_CONTACTS);
            boolean camera_perm = sharedPref.getBoolean(c.getString(R.string.pref_systemCamera_key), true);
            if (camera_perm && !checkPermission(c, Manifest.permission.CAMERA))
                perms.add(Manifest.permission.CAMERA);
            boolean sys_dialer = sharedPref.getBoolean(c.getString(R.string.pref_systemDialer_key), false);
            if (sys_dialer && !checkPermission(c, Manifest.permission.WRITE_CALL_LOG))
                perms.add(Manifest.permission.WRITE_CALL_LOG);
            return perms.toArray(new String[perms.size()]);
        }
    
        public IDRingService getRemoteService() {
            return mService;
        }
    
        public List<Account> getAccounts() { return accounts; }
        public Account getAccount(String account_id) {
            if (account_id == null || account_id.isEmpty())
                return null;
            for (Account acc : accounts)
                if (acc.getAccountID().equals(account_id))
                    return acc;
            return null;
        }
        public void setAccountOrder(List<String> accountOrder) {
            ArrayList<Account> newlist = new ArrayList<>(accounts.size());
            String order = "";
            for (String acc_id : accountOrder) {
                Account acc = getAccount(acc_id);
                if (acc != null)
                    newlist.add(acc);
                order += acc_id + File.separator;
            }
            accounts = newlist;
            try {
                mService.setAccountOrder(order);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            sendBroadcast(new Intent(ACTION_ACCOUNT_UPDATE));
        }
    
    
        public ArrayList<Conversation> getConversations() {
            ArrayList<Conversation> convs = new ArrayList<>(conversations.values());
            Collections.sort(convs, new Comparator<Conversation>() {
                @Override
                public int compare(Conversation lhs, Conversation rhs) {
                    return (int) ((rhs.getLastInteraction().getTime() - lhs.getLastInteraction().getTime())/1000l);
                }
            });
            return convs;
        }
    
        public Conversation getConversation(String id) {
            return conversations.get(id);
        }
    
        public Conference getConference(String id) {
            for (Conversation conv : conversations.values()) {
                Conference conf = conv.getConference(id);
                if (conf != null)
                    return conf;
            }
            return null;
        }
    
        public Pair<Conference, SipCall> getCall(String id) {
            for (Conversation conv : conversations.values()) {
                ArrayList<Conference> confs = conv.getCurrentCalls();
                for (Conference c : confs) {
                    SipCall call = c.getCallById(id);
                    if (call != null)
                        return new Pair<>(c, call);
                }
            }
            return new Pair<>(null, null);
        }
    
        public Conversation getByContact(CallContact contact) {
            ArrayList<String> keys = contact.getIds();
            for (String k : keys) {
                Conversation c = conversations.get(k);
                if (c != null)
                    return c;
            }
            Log.w(TAG, "getByContact failed");
            return null;
        }
        public Conversation getConversationByCallId(String callId) {
            for (Conversation conv : conversations.values()) {
                Conference conf = conv.getConference(callId);
                if (conf != null)
                    return conv;
            }
            return null;
        }
    
        public Conversation startConversation(CallContact contact) {
            if (contact.isUnknown())
                contact = findContactByNumber(contact.getPhones().get(0).getNumber());
            Conversation c = getByContact(contact);
            if (c == null) {
                c = new Conversation(contact);
                conversations.put(contact.getIds().get(0), c);
            }
            return c;
        }
    
        public CallContact findContactByNumber(SipUri number) {
            for (Conversation conv : conversations.values()) {
                if (conv.contact.hasNumber(number))
                    return conv.contact;
            }
            return canUseContacts ? findContactByNumber(getContentResolver(), number.getRawUriString()) : CallContact.buildUnknown(number);
        }
    
        public Conversation findConversationByNumber(SipUri number) {
            if (number == null || number.isEmpty())
                return null;
            for (Conversation conv : conversations.values()) {
                if (conv.contact.hasNumber(number))
                    return conv;
            }
            return startConversation(canUseContacts ? findContactByNumber(getContentResolver(), number.getRawUriString()) : CallContact.buildUnknown(number));
        }
    
        public CallContact findContactById(long id) {
            if (id <= 0)
                return null;
            CallContact c = systemContactCache.get(id);
            if (c == null) {
                Log.w(TAG, "getContactById : cache miss for " + id);
                c = findById(getContentResolver(), id, null);
                systemContactCache.put(id, c);
            }
            return c;
        }
    
        public Account guessAccount(SipUri uri) {
            if (uri.isRingId()) {
                for (Account a : accounts)
                    if (a.isRing())
                        return a;
                // ring ids must be called with ring accounts
                return null;
            }
            for (Account a : accounts)
                if (a.isSip() && a.getHost().equals(uri.host))
                    return a;
            if (uri.isSingleIp()) {
                for (Account a : accounts)
                    if (a.isIP2IP())
                        return a;
            }
            return accounts.get(0);
        }
    
        public void clearHistory() {
            historyManager.clearDB();
            refreshConversations();
        }
    
        public static final String[] DATA_PROJECTION = {
                ContactsContract.Data._ID,
                ContactsContract.RawContacts.CONTACT_ID,
                ContactsContract.Data.LOOKUP_KEY,
                ContactsContract.Data.DISPLAY_NAME_PRIMARY,
                ContactsContract.Data.PHOTO_ID,
                ContactsContract.Data.PHOTO_THUMBNAIL_URI,
                ContactsContract.Data.STARRED
        };
        public static final String[] CONTACT_PROJECTION = {
                ContactsContract.Contacts._ID,
                ContactsContract.Contacts.LOOKUP_KEY,
                ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
                ContactsContract.Contacts.PHOTO_ID,
                ContactsContract.Contacts.STARRED
        };
    
        public static final String[] PHONELOOKUP_PROJECTION = {
                ContactsContract.PhoneLookup._ID,
                ContactsContract.PhoneLookup.LOOKUP_KEY,
                ContactsContract.PhoneLookup.PHOTO_ID,
                ContactsContract.Contacts.DISPLAY_NAME_PRIMARY
        };
    
        private static final String[] CONTACTS_PHONES_PROJECTION = {
                Phone.NUMBER,
                Phone.TYPE,
                Phone.LABEL
        };
    
        private static final String[] CONTACTS_SIP_PROJECTION = {
                ContactsContract.Data.MIMETYPE,
                SipAddress.SIP_ADDRESS,
                SipAddress.TYPE,
                SipAddress.LABEL
        };
    
        private static final String ID_SELECTION = ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=?";
    
        private static void lookupDetails(@NonNull ContentResolver res, @NonNull CallContact c) {
            //Log.w(TAG, "lookupDetails " + c.getKey());
            try {
                Cursor cPhones = res.query(
                        ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                        CONTACTS_PHONES_PROJECTION, ID_SELECTION,
                        new String[]{String.valueOf(c.getId())}, null);
                if (cPhones != null) {
                    final int iNum =  cPhones.getColumnIndex(Phone.NUMBER);
                    final int iType =  cPhones.getColumnIndex(Phone.TYPE);
                    final int iLabel =  cPhones.getColumnIndex(Phone.LABEL);
                    while (cPhones.moveToNext()) {
                        c.addNumber(cPhones.getString(iNum), cPhones.getInt(iType), cPhones.getString(iLabel), CallContact.NumberType.TEL);
                        Log.w(TAG,"Phone:"+cPhones.getString(cPhones.getColumnIndex(Phone.NUMBER)));
                    }
                    cPhones.close();
                }
    
                Uri baseUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, c.getId());
                Uri targetUri = Uri.withAppendedPath(baseUri, ContactsContract.Contacts.Data.CONTENT_DIRECTORY);
                Cursor cSip = res.query(targetUri,
                        CONTACTS_SIP_PROJECTION,
                        ContactsContract.Data.MIMETYPE + "=? OR " + ContactsContract.Data.MIMETYPE + " =?",
                        new String[]{SipAddress.CONTENT_ITEM_TYPE, Im.CONTENT_ITEM_TYPE}, null);
                if (cSip != null) {
                    final int iMime = cSip.getColumnIndex(ContactsContract.Data.MIMETYPE);
                    final int iSip =  cSip.getColumnIndex(SipAddress.SIP_ADDRESS);
                    final int iType =  cSip.getColumnIndex(SipAddress.TYPE);
                    final int iLabel =  cSip.getColumnIndex(SipAddress.LABEL);
                    while (cSip.moveToNext()) {
                        String mime = cSip.getString(iMime);
                        String number = cSip.getString(iSip);
                        if (!mime.contentEquals(Im.CONTENT_ITEM_TYPE) || new SipUri(number).isRingId() || "ring".equalsIgnoreCase(cSip.getString(iLabel)))
                            c.addNumber(number, cSip.getInt(iType), cSip.getString(iLabel), CallContact.NumberType.SIP);
                        Log.w(TAG, "SIP phone:" + number + " " + mime + " ");
                    }
                    cSip.close();
                }
            } catch(Exception e) {
                Log.w(TAG, e);
            }
        }
    
        public static CallContact findById(@NonNull ContentResolver res, long id, String key) {
            CallContact contact = null;
            try {
                Uri contentUri;
                if (key != null)
                    contentUri = ContactsContract.Contacts.lookupContact(res, ContactsContract.Contacts.getLookupUri(id, key));
                else
                    contentUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
    
                Cursor result = res.query(contentUri, CONTACT_PROJECTION, null, null, null);
                if (result == null)
                    return null;
    
                if (result.moveToFirst()) {
                    int iID = result.getColumnIndex(ContactsContract.Data._ID);
                    int iKey = result.getColumnIndex(ContactsContract.Data.LOOKUP_KEY);
                    int iName = result.getColumnIndex(ContactsContract.Data.DISPLAY_NAME);
                    int iPhoto = result.getColumnIndex(ContactsContract.Data.PHOTO_ID);
                    int iStared = result.getColumnIndex(ContactsContract.Contacts.STARRED);
                    long cid = result.getLong(iID);
    
                    Log.w(TAG, "Contact name: " + result.getString(iName) + " id:" + cid + " key:" + result.getString(iKey));
    
                    contact = new CallContact(cid, result.getString(iKey), result.getString(iName), result.getLong(iPhoto));
                    if (result.getInt(iStared) != 0)
                        contact.setStared();
                    lookupDetails(res, contact);
                }
                result.close();
            } catch (Exception e) {
                Log.w(TAG, e);
            }
            if (contact == null)
                Log.w(TAG, "findById " + id + " can't find contact.");
            return contact;
        }
    
        @NonNull
        public static CallContact findContactBySipNumber(@NonNull ContentResolver res, String number) {
            ArrayList<CallContact> contacts = new ArrayList<>(1);
            try {
                Cursor result = res.query(ContactsContract.Data.CONTENT_URI,
                        DATA_PROJECTION,
                        SipAddress.SIP_ADDRESS + "=?" + " AND (" + ContactsContract.Data.MIMETYPE + "=? OR " + ContactsContract.Data.MIMETYPE + "=?)",
                        new String[]{number, SipAddress.CONTENT_ITEM_TYPE, Im.CONTENT_ITEM_TYPE}, null);
                if (result == null)  {
                    Log.w(TAG, "findContactBySipNumber " + number + " can't find contact.");
                    return CallContact.buildUnknown(number);
                }
                int icID = result.getColumnIndex(ContactsContract.RawContacts.CONTACT_ID);
                int iKey = result.getColumnIndex(ContactsContract.Data.LOOKUP_KEY);
                int iName = result.getColumnIndex(ContactsContract.Data.DISPLAY_NAME);
                int iPhoto = result.getColumnIndex(ContactsContract.Data.PHOTO_ID);
                int iPhotoThumb = result.getColumnIndex(ContactsContract.Data.PHOTO_THUMBNAIL_URI);
                int iStared = result.getColumnIndex(ContactsContract.Contacts.STARRED);
    
                while (result.moveToNext()) {
                    long cid = result.getLong(icID);
                    CallContact contact = new CallContact(cid, result.getString(iKey), result.getString(iName), result.getLong(iPhoto));
                    if (result.getInt(iStared) != 0)
                        contact.setStared();
                    lookupDetails(res, contact);
                    contacts.add(contact);
                }
                result.close();
                //lookupDetails(res, contact);
            } catch(Exception e) {
                Log.w(TAG, e);
            }
            if (contacts.isEmpty() || contacts.get(0).getPhones().isEmpty()) {
                Log.w(TAG, "findContactBySipNumber " + number + " can't find contact.");
                return CallContact.buildUnknown(number);
            }
            return contacts.get(0);
        }
    
        @NonNull
        public static CallContact findContactByNumber(@NonNull ContentResolver res, String number) {
            //Log.w(TAG, "findContactByNumber " + number);
            CallContact c = null;
            try {
                Uri uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
                Cursor result = res.query(uri, PHONELOOKUP_PROJECTION, null, null, null);
                if (result == null)  {
                    Log.w(TAG, "findContactByNumber " + number + " can't find contact.");
                    return findContactBySipNumber(res, number);
                }
                if (result.moveToFirst())  {
                    int iID = result.getColumnIndex(ContactsContract.Contacts._ID);
                    int iKey = result.getColumnIndex(ContactsContract.Data.LOOKUP_KEY);
                    int iName = result.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
                    int iPhoto = result.getColumnIndex(ContactsContract.Contacts.PHOTO_ID);
                    c = new CallContact(result.getLong(iID), result.getString(iKey), result.getString(iName), result.getLong(iPhoto));
                    lookupDetails(res, c);
                    Log.w(TAG, "findContactByNumber " + number + " found " + c.getDisplayName());
                }
                result.close();
            } catch(Exception e) {
                Log.w(TAG, e);
            }
            if (c == null) {
                Log.w(TAG, "findContactByNumber " + number + " can't find contact.");
                c = findContactBySipNumber(res, number);
            }
            return c;
        }
    
        private class ConversationLoader extends AsyncTask<Void, Void, Map<String, Conversation>> {
            private final ContentResolver cr;
            private final LongSparseArray<CallContact> localContactCache;
            private final HashMap<String, CallContact> localNumberCache = new HashMap<>(64);
    
            public ConversationLoader(ContentResolver c, LongSparseArray<CallContact> cache) {
                cr = c;
                localContactCache = (cache == null) ? new LongSparseArray<CallContact>(64) : cache;
            }
    
            private CallContact getByNumber(HashMap<String, CallContact> cache, String number) {
                if (number == null || number.isEmpty())
                    return null;
                number = CallContact.canonicalNumber(number);
                CallContact c = cache.get(number);
                if (c == null) {
                    c = canUseContacts ? findContactByNumber(cr, number) : CallContact.buildUnknown(number);
                    //if (c != null)
                    cache.put(number, c);
                }
                return c;
            }
    
            Pair<HistoryEntry, HistoryCall> findHistoryByCallId(final Map<String, Conversation> confs, String id) {
                for (Conversation c : confs.values()) {
                    Pair<HistoryEntry, HistoryCall> h = c.findHistoryByCallId(id);
                    if (h != null)
                        return h;
                }
                return null;
            }
    
            CallContact getCreateContact(long contact_id, String contact_key, String cnumber) {
                String number = CallContact.canonicalNumber(cnumber);
                //Log.w(TAG, "getCreateContact : " + cnumber + " " + number + " " + contact_id + " " + contact_key);
                CallContact contact;
                if (contact_id <= CallContact.DEFAULT_ID) {
                    contact = getByNumber(localNumberCache, number);
                } else {
                    contact = localContactCache.get(contact_id);
                    if (contact == null) {
                        contact = canUseContacts ? findById(cr, contact_id, contact_key) : CallContact.buildUnknown(number);
                        if (contact != null)
                            contact.addPhoneNumber(cnumber);
                        else {
                            Log.w(TAG, "Can't find contact with id " + contact_id);
                            contact = getByNumber(localNumberCache, number);
                        }
                        localContactCache.put(contact.getId(), contact);
                    }
                }
                return contact;
            }
    
            @Override
            protected Map<String, Conversation> doInBackground(Void... params) {
                final Map<String, Conversation> ret = new HashMap<>();
                try {
                    final List<HistoryCall> history = historyManager.getAll();
                    final List<HistoryText> historyTexts = historyManager.getAllTextMessages();
                    final Map<String, ArrayList<String>> confs = mService.getConferenceList();
    
                    for (HistoryCall call : history) {
                        //Log.w(TAG, "History call : " + call.getNumber() + " " + call.call_start + " " + call.getEndDate().toString() + " " + call.getContactID());
                        CallContact contact = getCreateContact(call.getContactID(), call.getContactKey(), call.getNumber());
    
                        Map.Entry<String, Conversation> merge = null;
                        for (Map.Entry<String, Conversation> ce : ret.entrySet()) {
                            Conversation c = ce.getValue();
                            if ((contact.getId() > 0 && contact.getId() == c.contact.getId()) || c.contact.hasNumber(call.getNumber())) {
                                merge = ce;
                                break;
                            }
                        }
                        if (merge != null) {
                            Conversation c = merge.getValue();
                            //Log.w(TAG, "        Join to " + merge.getKey() + " " + c.getContact().getDisplayName() + " " + call.getNumber());
                            if (c.getContact().getId() <= 0 && contact.getId() > 0) {
                                c.contact = contact;
                                ret.remove(merge.getKey());
                                ret.put(contact.getIds().get(0), c);
                            }
                            c.addHistoryCall(call);
                            continue;
                        }
                        String key = contact.getIds().get(0);
                        if (ret.containsKey(key)) {
                            ret.get(key).addHistoryCall(call);
                        } else {
                            Conversation c = new Conversation(contact);
                            c.addHistoryCall(call);
                            ret.put(key, c);
                        }
                    }
    
                    for (HistoryText htext : historyTexts) {
                        //Log.w(TAG, "History text : " + htext.getNumber() + " " + htext.getDate() + " " + htext.getCallId() + " " + htext.getAccountID() + " " + htext.getMessage());
                        CallContact contact = getCreateContact(htext.getContactID(), htext.getContactKey(), htext.getNumber());
                        Pair<HistoryEntry, HistoryCall> p = findHistoryByCallId(ret, htext.getCallId());
    
                        if (contact == null && p != null)
                            contact = p.first.getContact();
                        if (contact == null)
                            continue;
    
                        TextMessage msg = new TextMessage(htext);
                        msg.setContact(contact);
    
                        if (p  != null) {
                            if (msg.getNumberUri() == null)
                                msg.setNumber(new SipUri(p.second.getNumber()));
                            p.first.addTextMessage(msg);
                        }
    
                        String key = contact.getIds().get(0);
                        if (ret.containsKey(key)) {
                            ret.get(key).addTextMessage(msg);
                        } else {
                            Conversation c = new Conversation(contact);
                            c.addTextMessage(msg);
                            ret.put(key, c);
                        }
                    }
    
                    for (Map.Entry<String, ArrayList<String>> c : confs.entrySet()) {
                        Conference conf = new Conference(c.getKey());
                        for (String call_id : c.getValue()) {
                            SipCall call = getCall(call_id).second;
                            if (call == null)
                                call = new SipCall(call_id, mService.getCallDetails(call_id));
                            Account acc = getAccount(call.getAccount());
                            if(acc.isRing()
                                    || acc.getSrtpDetails().getDetailBoolean(AccountDetailSrtp.CONFIG_SRTP_ENABLE)
                                    || acc.getTlsDetails().getDetailBoolean(AccountDetailTls.CONFIG_TLS_ENABLE)) {
                                call = new SecureSipCall(call, acc.getSrtpDetails().getDetailString(AccountDetailSrtp.CONFIG_SRTP_KEY_EXCHANGE));
                            }
                            conf.addParticipant(call);
                        }
                        List<SipCall> calls = conf.getParticipants();
                        if (calls.size() == 1) {
                            SipCall call = calls.get(0);
                            CallContact contact = getCreateContact(-1, null, call.getNumber());
                            call.setContact(contact);
    
                            Conversation conv = null;
                            ArrayList<String> ids = contact.getIds();
                            for (String id : ids) {
                                //Log.w(TAG, "    uri attempt : " + id);
                                conv = ret.get(id);
                                if (conv != null) break;
                            }
                            if (conv != null) {
                                conv.addConference(conf);
                            } else {
                                conv = new Conversation(contact);
                                conv.addConference(conf);
                                ret.put(ids.get(0), conv);
                            }
                        }
                    }
                    for (Conversation c : ret.values())
                        Log.w(TAG, "Conversation : " + c.getContact().getId() + " " + c.getContact().getDisplayName() + " " + c.getLastNumberUsed(c.getLastAccountUsed()) + " " + c.getLastInteraction().toString());
                    for (int i=0; i<localContactCache.size(); i++) {
                        CallContact contact = localContactCache.valueAt(i);
                        String key = contact.getIds().get(0);
                        if (!ret.containsKey(key))
                            ret.put(key, new Conversation(contact));
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return ret;
            }
        }
    
        private void updated(Map<String, Conversation> res) {
            for (Conversation conv : conversations.values()) {
                for (Conference c : conv.current_calls) {
                    notificationManager.cancel(c.notificationId);
                }
            }
            conversations = res;
            updateAudioState();
            updateTextNotifications();
            sendBroadcast(new Intent(ACTION_CONF_UPDATE));
            sendBroadcast(new Intent(ACTION_CONF_LOADED));
            this.mAreConversationsLoaded = true;
        }
    
        private void updateConnectivityState() {
            ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
    
            NetworkInfo ni = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
            Log.w(TAG, "ActiveNetworkInfo (Wifi): " + (ni == null ? "null" : ni.toString()));
            isWifiConn = ni != null && ni.isConnected();
    
            ni = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
            Log.w(TAG, "ActiveNetworkInfo (mobile): " + (ni == null ? "null" : ni.toString()));
            isMobileConn = ni != null && ni.isConnected();
    
            if (dringStarted) {
                try {
                    getRemoteService().setAccountsActive(isConnected());
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
    
                Log.d(TAG, "Sending broadcast");
                sendBroadcast(new Intent(ACTION_ACCOUNT_UPDATE));
            }
        }
    
        public void updateTextNotifications()
        {
            Log.d(TAG, "updateTextNotifications()");
    
            for (Conversation c : conversations.values()) {
                TreeMap<Long, TextMessage> texts = c.getUnreadTextMessages();
                if (texts.isEmpty() || texts.lastEntry().getValue().isNotified()) {
                    continue;
                } else
                    notificationManager.cancel(c.notificationId);
    
                CallContact contact = c.getContact();
                if (c.notificationBuilder == null) {
                    c.notificationBuilder = new NotificationCompat.Builder(getApplicationContext());
                    c.notificationBuilder.setCategory(NotificationCompat.CATEGORY_MESSAGE)
                            .setPriority(NotificationCompat.PRIORITY_HIGH)
                            .setDefaults(NotificationCompat.DEFAULT_ALL)
                            .setSmallIcon(R.drawable.ic_launcher)
                            .setContentTitle(contact.getDisplayName());
                }
                NotificationCompat.Builder noti = c.notificationBuilder;
                Intent c_intent = new Intent(Intent.ACTION_VIEW)
                        .setClass(this, ConversationActivity.class)
                        .setData(Uri.withAppendedPath(ConversationActivity.CONTENT_URI, contact.getIds().get(0)));
                Intent d_intent = new Intent(ACTION_CONV_READ)
                        .setClass(this, LocalService.class)
                        .setData(Uri.withAppendedPath(ConversationActivity.CONTENT_URI, contact.getIds().get(0)));
                noti.setContentIntent(PendingIntent.getActivity(this, new Random().nextInt(), c_intent, 0))
                        .setDeleteIntent(PendingIntent.getService(this, new Random().nextInt(), d_intent, 0));
    
                if (contact.getPhoto() != null) {
                    Resources res = getResources();
                    int height = (int) res.getDimension(android.R.dimen.notification_large_icon_height);
                    int width = (int) res.getDimension(android.R.dimen.notification_large_icon_width);
                    noti.setLargeIcon(Bitmap.createScaledBitmap(contact.getPhoto(), width, height, false));
                }
                if (texts.size() == 1) {
                    TextMessage txt = texts.firstEntry().getValue();
                    txt.setNotified(true);
                    noti.setContentText(txt.getMessage());
                    noti.setStyle(null);
                    noti.setWhen(txt.getTimestamp());
                } else {
                    NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
                    for (TextMessage s : texts.values()) {
                        inboxStyle.addLine(Html.fromHtml("<b>" + DateUtils.formatDateTime(this, s.getTimestamp(), DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL) + "</b> " + s.getMessage()));
                        s.setNotified(true);
                    }
                    noti.setContentText(texts.lastEntry().getValue().getMessage());
                    noti.setStyle(inboxStyle);
                    noti.setWhen(texts.lastEntry().getValue().getTimestamp());
                }
                notificationManager.notify(c.notificationId, noti.build());
            }
        }
    
        private void updateAudioState() {
            boolean current = false;
            Conference ringing = null;
            for (Conversation c : conversations.values()) {
                Conference conf = c.getCurrentCall();
                if (conf != null) {
                    current = true;
                    if (conf.isRinging() && conf.isIncoming()) {
                        ringing = conf;
                        break;
                    }
                }
            }
            if (current)
                mediaManager.obtainAudioFocus(ringing != null);
    
            if (ringing != null) {
                //Log.w(TAG, "updateAudioState Ringing ");
                mediaManager.audioManager.setMode(AudioManager.MODE_RINGTONE);
                mediaManager.startRing(null);
            } else if (current) {
                //Log.w(TAG, "updateAudioState communication ");
                mediaManager.stopRing();
                mediaManager.audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
            } else {
                //Log.w(TAG, "updateAudioState normal ");
                mediaManager.stopRing();
                mediaManager.abandonAudioFocus();
            }
        }
    
        private final BroadcastReceiver receiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                Log.w(TAG, "BroadcastReceiver onReceive " + intent.getAction());
                switch(intent.getAction()) {
                    case DRingService.DRING_CONNECTION_CHANGED: {
                        boolean connected = intent.getBooleanExtra("connected", false);
                        if (connected) {
                            dringStarted = true;
                            if (mService != null) {
                                mAccountLoader.startLoading();
                                mAccountLoader.onContentChanged();
                            }
                        } else {
                            Log.w(TAG, "DRing connection lost ");
                            dringStarted = false;
                        }
                        break;
                    }
                    case ACTION_CONV_READ: {
                        String convId = intent.getData().getLastPathSegment();
                        Conversation conversation = getConversation(convId);
                        if (conversation != null) {
                            readConversation(conversation);
                        }
                        sendBroadcast(new Intent(ACTION_CONF_UPDATE).setData(Uri.withAppendedPath(ConversationActivity.CONTENT_URI, convId)));
                        break;
                    }
                    case ACTION_CALL_ACCEPT: {
                        String callId = intent.getData().getLastPathSegment();
                        try {
                            mService.accept(callId);
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
                        updateAudioState();
                        Conference conf = getConference(callId);
                        if (conf != null && !conf.mVisible)
                            startActivity(conf.getViewIntent(LocalService.this).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
                        break;
                    }
                    case ACTION_CALL_REFUSE: {
                        String call_id = intent.getData().getLastPathSegment();
                        try {
                            mService.refuse(call_id);
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
                        updateAudioState();
                        break;
                    }
                    case ACTION_CALL_END: {
                        String call_id = intent.getData().getLastPathSegment();
                        try {
                            mService.hangUp(call_id);
                            mService.hangUpConference(call_id);
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
                        updateAudioState();
                        break;
                    }
                    case ConnectivityManager.CONNECTIVITY_ACTION:
                        Log.w(TAG, "ConnectivityManager.CONNECTIVITY_ACTION " + " " + intent.getStringExtra(ConnectivityManager.EXTRA_EXTRA_INFO) + " " + intent.getStringExtra(ConnectivityManager.EXTRA_EXTRA_INFO));
                        updateConnectivityState();
                        break;
                    case ConfigurationManagerCallback.ACCOUNT_STATE_CHANGED:
                        Log.w(TAG, "Received " + intent.getAction() + " " + intent.getStringExtra("account") + " " + intent.getStringExtra("state") + " " + intent.getIntExtra("code", 0));
                        if (mAccountLoader.isStarted()) {
                            mAccountLoader.cancelLoad();
                            mAccountLoader.stopLoading();
                            mAccountLoader.startLoading();
                            mAccountLoader.onContentChanged();
                        } else {
                            for (Account a : accounts) {
                                if (a.getAccountID().contentEquals(intent.getStringExtra("account"))) {
                                    a.setRegistrationState(intent.getStringExtra("state"), intent.getIntExtra("code", 0));
                                    sendBroadcast(new Intent(ACTION_ACCOUNT_UPDATE));
                                    break;
                                }
                            }
                        }
                        break;
                    case ConfigurationManagerCallback.ACCOUNTS_CHANGED:
                        mAccountLoader.startLoading();
                        mAccountLoader.onContentChanged();
                        break;
                    case CallManagerCallBack.INCOMING_TEXT:
                    case ConfigurationManagerCallback.INCOMING_TEXT: {
                        String message = intent.getStringExtra("txt");
                        String number = intent.getStringExtra("from");
                        String call = intent.getStringExtra("call");
                        String account = intent.getStringExtra("account");
                        TextMessage txt = new TextMessage(true, message, new SipUri(number), call, account);
                        Log.w(TAG, "New text messsage " + txt.getAccount() + " " + txt.getCallId() + " " + txt.getMessage());
    
                        Conversation conv;
                        if (call != null && !call.isEmpty()) {
                            conv = getConversationByCallId(call);
                        } else {
                            conv = startConversation(findContactByNumber(txt.getNumberUri()));
                            txt.setContact(conv.getContact());
                        }
                        if (conv.mVisible)
                            txt.read();
                        historyManager.insertNewTextMessage(txt);
    
                        conv.addTextMessage(txt);
                        if (!conv.mVisible)
                            updateTextNotifications();
    
                        sendBroadcast(new Intent(ACTION_CONF_UPDATE));
                        break;
                    }
                    case ConfigurationManagerCallback.MESSAGE_STATE_CHANGED: {
                        long id = intent.getLongExtra(ConfigurationManagerCallback.MESSAGE_STATE_CHANGED_EXTRA_ID, 0);
                        int status = intent.getIntExtra(
                                ConfigurationManagerCallback.MESSAGE_STATE_CHANGED_EXTRA_STATUS,
                                TextMessage.Status.UNKNOWN.toInt()
                        );
                        TextMessage msg = messages.get(id);
                        if (msg != null) {
                            Log.w(TAG, "Message status changed " + id + " " + status);
                            msg.setStatus(status);
                            sendBroadcast(new Intent(ACTION_CONF_UPDATE).
                                    putExtra(ACTION_CONF_UPDATE_EXTRA_MSG, id)
                            );
                        }
                        break;
                    }
                    case CallManagerCallBack.INCOMING_CALL: {
                        String callId = intent.getStringExtra("call");
                        String accountId = intent.getStringExtra("account");
                        SipUri number = new SipUri(intent.getStringExtra("from"));
                        CallContact contact = findContactByNumber(number);
                        Conversation conv = startConversation(contact);
    
                        SipCall call = new SipCall(callId, accountId, number, SipCall.Direction.INCOMING);
                        call.setContact(contact);
    
                        Account account = getAccount(accountId);
    
                        Conference toAdd;
                        if (account.useSecureLayer()) {
                            SecureSipCall secureCall = new SecureSipCall(call, account.getSrtpDetails().getDetailString(AccountDetailSrtp.CONFIG_SRTP_KEY_EXCHANGE));
                            toAdd = new Conference(secureCall);
                        } else {
                            toAdd = new Conference(call);
                        }
    
                        conv.addConference(toAdd);
                        toAdd.showCallNotification(LocalService.this);
                        updateAudioState();
    
                        try {
                            mService.setPreviewSettings();
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
    
                        sendBroadcast(new Intent(ACTION_CONF_UPDATE).setData(Uri.withAppendedPath(SipCall.CONTENT_URI, callId)));
                        break;
                    }
                    case CallManagerCallBack.CALL_STATE_CHANGED: {
                        String call_id = intent.getStringExtra("call");
                        Conversation conversation = null;
                        Conference found = null;
    
                        for (Conversation conv : conversations.values()) {
                            Conference tconf = conv.getConference(call_id);
                            if (tconf != null) {
                                conversation = conv;
                                found = tconf;
                                break;
                            }
                        }
    
                        if (found == null) {
                            Log.w(TAG, "CALL_STATE_CHANGED : Can't find conference " + call_id);
                        } else {
                            SipCall call = found.getCallById(call_id);
                            int old_state = call.getCallState();
                            int new_state = SipCall.stateFromString(intent.getStringExtra("state"));
    
                            Log.w(TAG, "Call state change for " + call_id + " : " + SipCall.stateToString(old_state) + " -> " + SipCall.stateToString(new_state));
    
                            if (new_state != old_state) {
                                Log.w(TAG, "CALL_STATE_CHANGED : updating call state to " + new_state);
                                if ((call.isRinging() || new_state == SipCall.State.CURRENT) && call.getTimestampStart() == 0) {
                                    call.setTimestampStart(System.currentTimeMillis());
                                }
                                call.setCallState(new_state);
                            }
    
                            try {
                                call.setDetails((HashMap<String, String>) intent.getSerializableExtra("details"));
                            } catch (Exception e) {
                                Log.w(TAG, "Can't set call details.", e);
                            }
    
                            if (new_state == SipCall.State.HUNGUP
                                    || new_state == SipCall.State.BUSY
                                    || new_state == SipCall.State.FAILURE
                                    || new_state == SipCall.State.INACTIVE
                                    || new_state == SipCall.State.OVER) {
                                if (new_state == SipCall.State.HUNGUP) {
                                    call.setTimestampEnd(System.currentTimeMillis());
                                }
                                historyManager.insertNewEntry(found);
                                conversation.addHistoryCall(new HistoryCall(call));
                                notificationManager.cancel(found.notificationId);
                                found.removeParticipant(call);
                            } else {
                                found.showCallNotification(LocalService.this);
                            }
                            if (new_state == SipCall.State.FAILURE || new_state == SipCall.State.BUSY) {
                                try {
                                    mService.hangUp(call_id);
                                } catch (RemoteException e) {
                                    e.printStackTrace();
                                }
                            }
                            if (found.getParticipants().isEmpty()) {
                                conversation.removeConference(found);
                            }
                        }
                        updateAudioState();
                        sendBroadcast(new Intent(ACTION_CONF_UPDATE));
                        break;
                    }
                    default:
                        refreshConversations();
                }
            }
        };
    
        public void startListener() {
            IntentFilter intentFilter = new IntentFilter();
    
            intentFilter.addAction(ACTION_CONV_READ);
    
            intentFilter.addAction(DRingService.DRING_CONNECTION_CHANGED);
    
            intentFilter.addAction(ConfigurationManagerCallback.ACCOUNT_STATE_CHANGED);
            intentFilter.addAction(ConfigurationManagerCallback.ACCOUNTS_CHANGED);
            intentFilter.addAction(ConfigurationManagerCallback.INCOMING_TEXT);
            intentFilter.addAction(ConfigurationManagerCallback.MESSAGE_STATE_CHANGED);
    
            intentFilter.addAction(CallManagerCallBack.INCOMING_CALL);
            intentFilter.addAction(CallManagerCallBack.INCOMING_TEXT);
            intentFilter.addAction(CallManagerCallBack.CALL_STATE_CHANGED);
            intentFilter.addAction(CallManagerCallBack.CONF_CREATED);
            intentFilter.addAction(CallManagerCallBack.CONF_CHANGED);
            intentFilter.addAction(CallManagerCallBack.CONF_REMOVED);
    
            intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
    
            registerReceiver(receiver, intentFilter);
    
            getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactContentObserver);
        }
    
        private class ContactsContentObserver extends ContentObserver {
    
            public ContactsContentObserver() {
                super(null);
            }
    
            @Override
            public void onChange(boolean selfChange, Uri uri) {
                super.onChange(selfChange, uri);
                Log.d(TAG, "ContactsContentObserver.onChange");
                refreshContacts();
            }
        }
    
        public void stopListener() {
            unregisterReceiver(receiver);
            getContentResolver().unregisterContentObserver(contactContentObserver);
        }
    
        public boolean areConversationsLoaded() {
            return mAreConversationsLoaded;
        }
    
        public void refreshContacts() {
            Log.d(TAG, "refreshContacts");
            mSystemContactLoader.onContentChanged();
            mSystemContactLoader.startLoading();
        }
    
        public void deleteConversation(Conversation conversation) {
            historyManager.clearHistoryForConversation(conversation);
            refreshConversations();
        }
    }