cx.ring.service.LocalService.java Source code

Java tutorial

Introduction

Here is the source code for cx.ring.service.LocalService.java

Source

/*
 *  Copyright (C) 2015-2016 Savoir-faire Linux Inc.
 *
 *  Author: Adrien Braud <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, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

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.RemoteException;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
import android.provider.ContactsContract.CommonDataKinds.Im;
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.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.fragments.SettingsFragment;
import cx.ring.history.HistoryCall;
import cx.ring.history.HistoryEntry;
import cx.ring.history.HistoryManager;
import cx.ring.history.HistoryText;
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.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_ACCOUNT_UPDATE = BuildConfig.APPLICATION_ID + ".action.ACCOUNT_UPDATE";
    static public final String ACTION_CONV_READ = BuildConfig.APPLICATION_ID + ".action.CONV_READ";

    // 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 String AUTHORITY = "cx.ring";
    public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
    public static final int PERMISSIONS_REQUEST = 57;

    public final static String[] REQUIRED_RUNTIME_PERMISSIONS = { Manifest.permission.RECORD_AUDIO };

    private IDRingService mService = null;
    private final ContactsContentObserver contactContentObserver = new ContactsContentObserver();

    // Binder given to clients
    private final IBinder mBinder = new LocalBinder();

    private Map<String, Conversation> conversations = new HashMap<>();
    private ArrayList<Account> all_accounts = new ArrayList<>();
    private List<Account> accounts = all_accounts;
    private List<Account> ip2ip_account = all_accounts;

    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;

    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 {
            SipUri number = call.getNumberUri();
            if (number == null || number.isEmpty())
                number = contact.getPhones().get(0).getNumber();
            String callId = mService.placeCall(call.getAccount(), number.getUriString());
            if (callId == null || callId.isEmpty()) {
                //CallActivity.this.terminateCall();
                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 {
            mService.sendAccountTextMessage(account, to.getRawUriString(), txt);
            TextMessage message = new TextMessage(false, txt, to, null, account);
            message.read();
            historyManager.insertNewTextMessage(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 void textMessageSent(TextMessage txt) {
        String call = txt.getCallId();
        Conversation conv;
        Log.w(TAG, "Sent text messsage " + txt.getAccount() + " " + txt.getCallId() + " " + txt.getNumberUri() + " "
                + txt.getMessage());
        if (call != null && !call.isEmpty()) {
            conv = getConversationByCallId(call);
            conv.addTextMessage(txt);
        } else {
            conv = startConversation(findContactByNumber(txt.getNumberUri()));
            txt.setContact(conv.getContact());
            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.e(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);
        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(SettingsFragment.KEY_PREF_CONTACTS, true);
        canUseMobile = sharedPreferences.getBoolean(SettingsFragment.KEY_PREF_MOBILE, true);
        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());
            all_accounts = data;
            accounts = all_accounts.subList(0, data.size() - 1);
            ip2ip_account = all_accounts.subList(data.size() - 1, data.size());
            boolean haveSipAccount = false;
            boolean haveRingAccount = false;
            for (Account acc : accounts) {
                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) {
        switch (key) {
        case SettingsFragment.KEY_PREF_CONTACTS:
            canUseContacts = sharedPreferences.getBoolean(key, true);
            mSystemContactLoader.onContentChanged();
            mSystemContactLoader.startLoading();
            break;
        case SettingsFragment.KEY_PREF_MOBILE:
            canUseMobile = sharedPreferences.getBoolean(key, true);
            updateConnectivityState();
            break;
        }
    }

    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);
            mAccountLoader.registerListener(1, onAccountsLoaded);
            try {
                if (mService.isStarted()) {
                    mAccountLoader.startLoading();
                    mAccountLoader.forceLoad();
                }
            } catch (RemoteException e) {
                e.printStackTrace();
            }

            mSystemContactLoader = new ContactsLoader(LocalService.this);
            mSystemContactLoader.registerListener(1, onSystemContactsLoaded);

            startListener();
        }

        @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;
    }

    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(SettingsFragment.KEY_PREF_CONTACTS, true);
        if (contact_perm && !checkPermission(c, Manifest.permission.READ_CONTACTS))
            perms.add(Manifest.permission.READ_CONTACTS);
        return perms.toArray(new String[perms.size()]);
    }

    public IDRingService getRemoteService() {
        return mService;
    }

    public List<Account> getAccounts() {
        return accounts;
    }

    public List<Account> getIP2IPAccount() {
        return ip2ip_account;
    }

    public Account getAccount(String account_id) {
        if (account_id == null || account_id.isEmpty())
            return null;
        for (Account acc : all_accounts)
            if (acc.getAccountID().equals(account_id))
                return acc;
        return null;
    }

    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(CallContact c, SipUri uri) {
        if (uri.isRingId()) {
            for (Account a : all_accounts)
                if (a.isRing())
                    return a;
            // ring ids must be called with ring accounts
            return null;
        }
        for (Account a : all_accounts)
            if (a.isSip() && a.getHost().equals(uri.host))
                return a;
        if (uri.isSingleIp())
            return ip2ip_account.get(0);
        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));
    }

    public class AccountsLoader extends AsyncTaskLoader<ArrayList<Account>> {
        public static final String ACCOUNTS = "accounts";
        public static final String ACCOUNT_IP2IP = "IP2IP";

        public AccountsLoader(Context context) {
            super(context);
            Log.w(TAG, "AccountsLoader constructor");
        }

        @SuppressWarnings("unchecked")
        @Override
        public ArrayList<Account> loadInBackground() {
            Log.w(TAG, "AccountsLoader loadInBackground");
            ArrayList<Account> accounts = new ArrayList<>();
            Account IP2IP = null;
            try {
                ArrayList<String> accountIDs = (ArrayList<String>) mService.getAccountList();
                Map<String, String> details;
                ArrayList<Map<String, String>> credentials;
                Map<String, String> state;
                for (String id : accountIDs) {
                    details = (Map<String, String>) mService.getAccountDetails(id);
                    state = (Map<String, String>) mService.getVolatileAccountDetails(id);
                    if (id.contentEquals(ACCOUNT_IP2IP)) {
                        IP2IP = new Account(ACCOUNT_IP2IP, details, new ArrayList<Map<String, String>>(), state); // Empty credentials
                        //accounts.add(IP2IP);
                        continue;
                    }
                    credentials = (ArrayList<Map<String, String>>) mService.getCredentials(id);
                    /*for (Map.Entry<String, String> entry : State.entrySet()) {
                        Log.i(TAG, "State:" + entry.getKey() + " -> " + entry.getValue());
                    }*/
                    Account tmp = new Account(id, details, credentials, state);
                    accounts.add(tmp);
                    // Log.i(TAG, "account:" + tmp.getAlias() + " " + tmp.isEnabled());
                }
            } catch (RemoteException | NullPointerException e) {
                Log.e(TAG, e.toString());
            }
            accounts.add(IP2IP);
            return accounts;
        }
    }

    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();

        try {
            getRemoteService().setAccountsActive(isConnected());
        } catch (RemoteException e) {
            e.printStackTrace();
        }

        // if account list loaded
        if (!ip2ip_account.isEmpty())
            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()) {
                    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) {
                    mAccountLoader.onContentChanged();
                    mAccountLoader.startLoading();
                    mAccountLoader.forceLoad();
                } else {
                    Log.w(TAG, "DRing connection lost ");
                }
                break;
            }
            case ACTION_CONV_READ: {
                String conv_id = intent.getData().getLastPathSegment();
                Conversation conversation = getConversation(conv_id);
                if (conversation != null) {
                    readConversation(conversation);
                }
                sendBroadcast(new Intent(ACTION_CONF_UPDATE));
                break;
            }
            case ACTION_CALL_ACCEPT: {
                String call_id = intent.getData().getLastPathSegment();
                try {
                    mService.accept(call_id);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                updateAudioState();
                Conference conf = getConference(call_id);
                if (!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));
                //accountStateChanged(intent.getStringExtra("Account"), intent.getStringExtra("State"), intent.getIntExtra("code", 0));
                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.onContentChanged();
                mAccountLoader.startLoading();
                mAccountLoader.forceLoad();
                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 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();

                sendBroadcast(new Intent(ACTION_CONF_UPDATE));
                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(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.w(TAG, "ContactsContentObserver.onChange");
            mSystemContactLoader.onContentChanged();
            mSystemContactLoader.startLoading();
        }
    }

    public void stopListener() {
        unregisterReceiver(receiver);
        getContentResolver().unregisterContentObserver(contactContentObserver);
    }

}