From f6146f831b1d620045ac48ad322e1f8b3117fe47 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrien=20B=C3=A9raud?= <adrien.beraud@savoirfairelinux.com>
Date: Tue, 20 Oct 2015 16:14:58 -0400
Subject: [PATCH] localservice: add contact cache and thread pool

* Optimize contact loading from system
* Cache results in LocalService
* Listen for system contact change to reload when needed
* Cache standard-size (40dp) contact pictures in LocalService
* Common thread pool to load contact pictures in LocalService

Issue: #78218
Change-Id: I61e28df8d020166933cc4fe24a207b0a760036b2
---
 .../cx/ring/adapters/ContactsAdapter.java     |  93 +-----
 .../ring/fragments/ConferenceDFragment.java   |   2 +-
 .../ring/fragments/ContactListFragment.java   | 159 ++--------
 .../cx/ring/fragments/TransferDFragment.java  |   2 +-
 .../java/cx/ring/loaders/ContactsLoader.java  | 209 ++++++++-----
 .../java/cx/ring/service/LocalService.java    | 279 +++++++++++++-----
 6 files changed, 380 insertions(+), 364 deletions(-)

diff --git a/ring-android/app/src/main/java/cx/ring/adapters/ContactsAdapter.java b/ring-android/app/src/main/java/cx/ring/adapters/ContactsAdapter.java
index 1754589c0..231e05efb 100644
--- a/ring-android/app/src/main/java/cx/ring/adapters/ContactsAdapter.java
+++ b/ring-android/app/src/main/java/cx/ring/adapters/ContactsAdapter.java
@@ -44,6 +44,8 @@ import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
 
 import android.content.Context;
 import android.graphics.Bitmap;
+import android.util.Log;
+import android.util.LongSparseArray;
 import android.util.LruCache;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -57,13 +59,12 @@ import android.widget.SectionIndexer;
 import android.widget.TextView;
 
 public class ContactsAdapter extends BaseAdapter implements StickyListHeadersAdapter, SectionIndexer {
-    private ExecutorService infos_fetcher = Executors.newCachedThreadPool();
-    Context mContext;
-
+    private final ExecutorService infos_fetcher;
+    private final Context mContext;
     private ArrayList<CallContact> mContacts;
     private int[] mSectionIndices;
     private Character[] mSectionLetters;
-    WeakReference<ContactListFragment> parent;
+    WeakReference<ContactListFragment.Callbacks> parent;
     private LayoutInflater mInflater;
 
     final private LruCache<Long, Bitmap> mMemoryCache;
@@ -71,22 +72,16 @@ public class ContactsAdapter extends BaseAdapter implements StickyListHeadersAda
 
     private static final String TAG = ContactsAdapter.class.getSimpleName();
 
-    public ContactsAdapter(ContactListFragment contactListFragment) {
+    public ContactsAdapter(Context c, ContactListFragment.Callbacks cb, LruCache<Long, Bitmap> cache, ExecutorService pool) {
         super();
-        mContext = contactListFragment.getActivity();
+        mContext = c;
         mInflater = LayoutInflater.from(mContext);
-        parent = new WeakReference<>(contactListFragment);
+        parent = new WeakReference<>(cb);
         mContacts = new ArrayList<>();
         mSectionIndices = getSectionIndices();
         mSectionLetters = getSectionLetters();
-        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;
-            }
-        };
+        mMemoryCache = cache;
+        infos_fetcher = pool;
     }
 
     public static final int TYPE_HEADER = 0;
@@ -142,7 +137,7 @@ public class ContactsAdapter extends BaseAdapter implements StickyListHeadersAda
 
         final CallContact item = mContacts.get(position);
 
-        if (entryView.position == position || (entryView.contact != null && entryView.contact.get() != null && item.getId() == entryView.contact.get().getId()))
+        if (/*entryView.position == position &&*/ (entryView.contact != null && entryView.contact.get() != null && item.getId() == entryView.contact.get().getId()))
             return convertView;
 
         entryView.display_name.setText(item.getDisplayName());
@@ -187,58 +182,10 @@ public class ContactsAdapter extends BaseAdapter implements StickyListHeadersAda
         convertView.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
-                parent.get().mCallbacks.onTextContact(item);
-            }
-        });
-
-/*
-        entryView.quick_call.setOnClickListener(new OnClickListener() {
-
-            @Override
-            public void onClick(View v) {
-                parent.get().mCallbacks.onCallContact(item);
-
-            }
-        });
-
-        entryView.quick_msg.setOnClickListener(new OnClickListener() {
-
-            @Override
-            public void onClick(View v) {
-                parent.get().mCallbacks.onTextContact(item);
-            }
-        });
-
-        entryView.quick_starred.setOnClickListener(new OnClickListener() {
-
-            @Override
-            public void onClick(View v) {
-                Toast.makeText(mContext, "Coming soon", Toast.LENGTH_SHORT).show();
-            }
-        });
-
-        entryView.quick_edit.setOnClickListener(new OnClickListener() {
-
-            @Override
-            public void onClick(View v) {
-                parent.get().mCallbacks.onEditContact(item);
-
-            }
-        });
-
-        entryView.quick_discard.setOnClickListener(new OnClickListener() {
-
-            @Override
-            public void onClick(View v) {
-                Toast.makeText(mContext, "Coming soon", Toast.LENGTH_SHORT).show();
-
+                parent.get().onTextContact(item);
             }
         });
 
-        entryView.quick_edit.setClickable(false);
-        entryView.quick_discard.setClickable(false);
-        entryView.quick_starred.setClickable(false);
-*/
         return convertView;
     }
 
@@ -335,23 +282,9 @@ public class ContactsAdapter extends BaseAdapter implements StickyListHeadersAda
         mContacts = new ArrayList<>();
         mSectionIndices = new int[0];
         mSectionLetters = new Character[0];
-        notifyDataSetChanged();
-    }
-/*
-    public void restore() {
-        mContacts = new ArrayList<>();
-        mSectionIndices = getSectionIndices();
-        mSectionLetters = getSectionLetters();
-        notifyDataSetChanged();
+        //notifyDataSetChanged();
     }
 
-    public void addAll(ArrayList<CallContact> tmp) {
-        mContacts.addAll(tmp);
-        mSectionIndices = getSectionIndices();
-        mSectionLetters = getSectionLetters();
-        notifyDataSetChanged();
-    }*/
-
     public void setData(ArrayList<CallContact> contacts) {
         mContacts = contacts;
         mSectionIndices = getSectionIndices();
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/ConferenceDFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/ConferenceDFragment.java
index 1e7075128..c0c0a69db 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/ConferenceDFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/ConferenceDFragment.java
@@ -95,7 +95,7 @@ public class ConferenceDFragment extends DialogFragment implements LoaderManager
         } else {
             baseUri = Contacts.CONTENT_URI;
         }
-        ContactsLoader l = new ContactsLoader(getActivity(), baseUri);
+        ContactsLoader l = new ContactsLoader(getActivity());
         l.forceLoad();
         return l;
     }
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/ContactListFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/ContactListFragment.java
index 07d33fd38..02ce5e7af 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/ContactListFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/ContactListFragment.java
@@ -31,14 +31,13 @@
  */
 package cx.ring.fragments;
 
-import java.util.ArrayList;
-
 import cx.ring.R;
 import cx.ring.adapters.ContactsAdapter;
 import cx.ring.adapters.StarredContactsAdapter;
 import cx.ring.loaders.ContactsLoader;
 import cx.ring.loaders.LoaderConstants;
 import cx.ring.model.CallContact;
+import cx.ring.service.LocalService;
 import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
 
 import android.app.Activity;
@@ -54,7 +53,6 @@ import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.View.DragShadowBuilder;
 import android.view.View.MeasureSpec;
 import android.view.View.OnTouchListener;
 import android.view.ViewGroup;
@@ -64,16 +62,16 @@ import android.widget.AdapterView.OnItemLongClickListener;
 import android.widget.GridView;
 import android.widget.LinearLayout;
 import android.widget.ListAdapter;
-import android.widget.RelativeLayout;
 import android.widget.SearchView;
 import android.widget.SearchView.OnQueryTextListener;
 import android.widget.TextView;
 
+import java.util.ArrayList;
+
 public class ContactListFragment extends Fragment implements OnQueryTextListener, LoaderManager.LoaderCallbacks<ContactsLoader.Result> {
     public static final String TAG = "ContactListFragment";
     ContactsAdapter mListAdapter;
     StarredContactsAdapter mGridAdapter;
-    //SearchView mQuickReturnSearchView;
     String mCurFilter;
     StickyListHeadersListView mContactList;
 
@@ -81,15 +79,12 @@ public class ContactListFragment extends Fragment implements OnQueryTextListener
     private LinearLayout llMain;
     private GridView mStarredGrid;
     private TextView favHeadLabel;
-    //private SwipeListViewTouchListener mSwipeLvTouchListener;
     private LinearLayout mHeader;
     private ViewGroup newcontact;
 
     @Override
     public void onCreate(Bundle savedInBundle) {
         super.onCreate(savedInBundle);
-        mGridAdapter = new StarredContactsAdapter(getActivity());
-        mListAdapter = new ContactsAdapter(this);
         setHasOptionsMenu(true);
     }
 
@@ -97,38 +92,19 @@ public class ContactListFragment extends Fragment implements OnQueryTextListener
     /**
      * A dummy implementation of the {@link Callbacks} interface that does nothing. Used only when this fragment is not attached to an activity.
      */
-    private static final Callbacks sDummyCallbacks = new Callbacks() {
+    private static class DummyCallbacks extends LocalService.DummyCallbacks implements Callbacks {
         @Override
         public void onCallContact(CallContact c) {
         }
         @Override
         public void onTextContact(CallContact c) {
         }
-        @Override
-        public void onEditContact(CallContact c) {
-        }
-        @Override
-        public void onContactDragged() {
-        }
-        @Override
-        public void toggleDrawer() {
-        }
-        @Override
-        public void setDragView(RelativeLayout relativeLayout) {
-        }
-        @Override
-        public void toggleForSearchDrawer() {
-        }
     };
+    private static final Callbacks sDummyCallbacks = new DummyCallbacks();
 
-    public interface Callbacks {
+    public interface Callbacks extends LocalService.Callbacks {
         void onCallContact(CallContact c);
         void onTextContact(CallContact c);
-        void onContactDragged();
-        void toggleDrawer();
-        void onEditContact(CallContact item);
-        void setDragView(RelativeLayout relativeLayout);
-        void toggleForSearchDrawer();
     }
 
     @Override
@@ -138,6 +114,8 @@ public class ContactListFragment extends Fragment implements OnQueryTextListener
             throw new IllegalStateException("Activity must implement fragment's callbacks.");
         }
         mCallbacks = (Callbacks) activity;
+        mGridAdapter = new StarredContactsAdapter(getActivity());
+        mListAdapter = new ContactsAdapter(getActivity(), mCallbacks, mCallbacks.getService().get40dpContactCache(), mCallbacks.getService().getThreadPool());
     }
 
     @Override
@@ -158,40 +136,8 @@ public class ContactListFragment extends Fragment implements OnQueryTextListener
         View inflatedView = inflater.inflate(R.layout.frag_contact_list, container, false);
         mHeader = (LinearLayout) inflater.inflate(R.layout.frag_contact_list_header, null);
         mContactList = (StickyListHeadersListView) inflatedView.findViewById(R.id.contacts_stickylv);
-        //mContactList.setDividerHeight(0);
         mContactList.setDivider(null);
 
-        inflatedView.findViewById(R.id.drag_view).setOnTouchListener(new OnTouchListener() {
-
-            @Override
-            public boolean onTouch(View v, MotionEvent event) {
-                return true;
-            }
-        });
-
-        /*inflatedView.findViewById(R.id.contact_search_button).setOnClickListener(new OnClickListener() {
-
-            @Override
-            public void onClick(View v) {
-                mContactList.smoothScrollToPosition(0);
-                mQuickReturnSearchView.setOnQueryTextListener(ContactListFragment.this);
-                mQuickReturnSearchView.setIconified(false);
-                mQuickReturnSearchView.setFocusable(true);
-                mCallbacks.toggleForSearchDrawer();
-            }
-        });
-
-        inflatedView.findViewById(R.id.slider_button).setOnClickListener(new OnClickListener() {
-
-            @Override
-            public void onClick(View v) {
-                mCallbacks.toggleDrawer();
-            }
-        });
-        
-        mCallbacks.setDragView(((RelativeLayout) inflatedView.findViewById(R.id.slider_button)));
-*/
-        //mQuickReturnSearchView = (SearchView) mHeader.findViewById(R.id.contact_search);
         mStarredGrid = (GridView) mHeader.findViewById(R.id.favorites_grid);
         llMain = (LinearLayout) mHeader.findViewById(R.id.llMain);
         favHeadLabel = (TextView) mHeader.findViewById(R.id.fav_head_label);
@@ -218,29 +164,14 @@ public class ContactListFragment extends Fragment implements OnQueryTextListener
         mContactList.setAdapter(mListAdapter);
 
         mStarredGrid.setAdapter(mGridAdapter);
-        /*mQuickReturnSearchView.setIconifiedByDefault(false);
-
-        mQuickReturnSearchView.setOnClickListener(new OnClickListener() {
-
-            @Override
-            public void onClick(View v) {
-                mQuickReturnSearchView.setIconified(false);
-                mQuickReturnSearchView.setFocusable(true);
-            }
-        });
-        mQuickReturnSearchView.setOnQueryTextListener(ContactListFragment.this);*/
-
-        getLoaderManager().initLoader(LoaderConstants.CONTACT_LOADER, null, this);
 
+        onLoadFinished(null, mCallbacks.getService().getSortedContacts());
     }
 
     private OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() {
         @Override
         public boolean onItemLongClick(AdapterView<?> av, View view, int pos, long id) {
-            DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view.findViewById(R.id.photo));
-            view.startDrag(null, shadowBuilder, view, 0);
-            mCallbacks.onContactDragged();
-            return true;
+            return false;
         }
 
     };
@@ -258,64 +189,15 @@ public class ContactListFragment extends Fragment implements OnQueryTextListener
     }
 
     private void setListViewListeners() {
-        /*mSwipeLvTouchListener = new SwipeListViewTouchListener(mContactList.getWrappedList(), new SwipeListViewTouchListener.OnSwipeCallback() {
-            @Override
-            public void onSwipeLeft(ListView listView, int[] reverseSortedPositions) {
-            }
-
-            @Override
-            public void onSwipeRight(ListView listView, View down) {
-                down.findViewById(R.id.quick_edit).setClickable(true);
-                down.findViewById(R.id.quick_discard).setClickable(true);
-                down.findViewById(R.id.quick_starred).setClickable(true);
-
-            }
-        }, true, false);*/
-
-        /*mContactList.getWrappedList().setOnDragListener(dragListener);
-        mContactList.getWrappedList().setOnTouchListener(mSwipeLvTouchListener);*/
         mContactList.getWrappedList().setOnItemLongClickListener(mItemLongClickListener);
-        /*mContactList.getWrappedList().setOnItemClickListener(new OnItemClickListener() {
-
-            @Override
-            public void onItemClick(AdapterView<?> arg0, View view, int pos, long id) {
-                Log.i(TAG, "Opening Item");
-                mSwipeLvTouchListener.openItem(view, pos, id);
-            }
-        });*/
     }
-/*
-    OnDragListener dragListener = new OnDragListener() {
-
-        @Override
-        public boolean onDrag(View v, DragEvent event) {
-            switch (event.getAction()) {
-            case DragEvent.ACTION_DRAG_STARTED:
-                // Do nothing
-                break;
-            case DragEvent.ACTION_DRAG_ENTERED:
-                break;
-            case DragEvent.ACTION_DRAG_EXITED:
-                // v.setBackgroundDrawable(null);
-                break;
-            case DragEvent.ACTION_DROP:
-                break;
-            case DragEvent.ACTION_DRAG_ENDED:
-                View view1 = (View) event.getLocalState();
-                view1.setVisibility(View.VISIBLE);
-            default:
-                break;
-            }
-            return true;
-        }
-
-    };*/
 
     @Override
     public boolean onQueryTextChange(String newText) {
         mCurFilter = newText;
         if (newText.isEmpty()) {
-            getLoaderManager().restartLoader(LoaderConstants.CONTACT_LOADER, null, this);
+            getLoaderManager().destroyLoader(LoaderConstants.CONTACT_LOADER);
+            onLoadFinished(null, mCallbacks.getService().getSortedContacts());
             newcontact.setVisibility(View.GONE);
             return true;
         }
@@ -339,33 +221,28 @@ public class ContactListFragment extends Fragment implements OnQueryTextListener
 
     @Override
     public Loader<ContactsLoader.Result> onCreateLoader(int id, Bundle args) {
-        Uri baseUri;
-
-        Log.i(TAG, "createLoader");
-
-        if (args != null) {
+        Uri baseUri = null;
+        if (args != null)
             baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(args.getString("filter")));
-        } else {
-            baseUri = Contacts.CONTENT_URI;
-        }
-        ContactsLoader l = new ContactsLoader(getActivity(), baseUri);
+        ContactsLoader l = new ContactsLoader(getActivity(), baseUri, mCallbacks.getService().getContactCache());
         l.forceLoad();
         return l;
     }
 
     @Override
     public void onLoadFinished(Loader<ContactsLoader.Result> loader, ContactsLoader.Result data) {
+        Log.i(TAG, "onLoadFinished with " + data.contacts.size() + " contacts, " + data.starred.size() + " starred.");
+
         mListAdapter.setData(data.contacts);
         setListViewListeners();
 
+        mGridAdapter.setData(data.starred);
         if (data.starred.isEmpty()) {
             llMain.setVisibility(View.GONE);
             favHeadLabel.setVisibility(View.GONE);
-            mGridAdapter.removeAll();
         } else {
             llMain.setVisibility(View.VISIBLE);
             favHeadLabel.setVisibility(View.VISIBLE);
-            mGridAdapter.setData(data.starred);
             setGridViewListeners();
             mStarredGrid.post(new Runnable() {
 
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/TransferDFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/TransferDFragment.java
index a2d8a57a4..05a663e1c 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/TransferDFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/TransferDFragment.java
@@ -173,7 +173,7 @@ public class TransferDFragment extends DialogFragment implements LoaderManager.L
         } else {
             baseUri = Contacts.CONTENT_URI;
         }
-        ContactsLoader l = new ContactsLoader(getActivity(), baseUri);
+        ContactsLoader l = new ContactsLoader(getActivity());
         l.forceLoad();
         return l;
     }
diff --git a/ring-android/app/src/main/java/cx/ring/loaders/ContactsLoader.java b/ring-android/app/src/main/java/cx/ring/loaders/ContactsLoader.java
index fd71e80af..961f4e5f0 100644
--- a/ring-android/app/src/main/java/cx/ring/loaders/ContactsLoader.java
+++ b/ring-android/app/src/main/java/cx/ring/loaders/ContactsLoader.java
@@ -1,7 +1,8 @@
 /*
- *  Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *  Copyright (C) 2015 Savoir-faire Linux Inc.
  *
  *  Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *  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
@@ -15,18 +16,7 @@
  *
  *  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.
- *
- *  Additional permission under GNU GPL version 3 section 7:
- *
- *  If you modify this program, or any covered work, by linking or
- *  combining it with the OpenSSL project's OpenSSL library (or a
- *  modified version of that library), containing parts covered by the
- *  terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
- *  grants you additional permission to convey the resulting work.
- *  Corresponding Source for a non-source form of such a combination
- *  shall include the source code for the parts of OpenSSL used as well
- *  as that of the covered work.
+ *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  */
 
 package cx.ring.loaders;
@@ -40,85 +30,172 @@ import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.Bundle;
+import android.os.OperationCanceledException;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
 import android.provider.ContactsContract.Contacts;
 import android.util.Log;
+import android.util.LongSparseArray;
 
 public class ContactsLoader extends AsyncTaskLoader<ContactsLoader.Result>
 {
     private static final String TAG = ContactsLoader.class.getSimpleName();
 
-    public class Result {
-        public final ArrayList<CallContact> contacts = new ArrayList<>();
+    public static class Result {
+        public final ArrayList<CallContact> contacts = new ArrayList<>(512);
         public final ArrayList<CallContact> starred = new ArrayList<>();
     }
 
-    // These are the Contacts rows that we will retrieve.
-    static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] { Contacts._ID, Contacts.LOOKUP_KEY, Contacts.DISPLAY_NAME, Contacts.PHOTO_ID, Contacts.STARRED };
-    static final String[] CONTACTS_PHONES_PROJECTION = new String[] { Phone.NUMBER, Phone.TYPE };
-    static final String[] CONTACTS_SIP_PROJECTION = new String[] { SipAddress.SIP_ADDRESS, SipAddress.TYPE };
+    static private final String[] CONTACTS_ID_PROJECTION = new String[] { Contacts._ID };
+    static private final String[] CONTACTS_SUMMARY_PROJECTION = new String[] { Contacts._ID, Contacts.LOOKUP_KEY, Contacts.DISPLAY_NAME, Contacts.PHOTO_ID, Contacts.STARRED};
+    static private final String[] CONTACTS_SIP_PROJECTION = new String[] { ContactsContract.CommonDataKinds.Phone.CONTACT_ID, ContactsContract.Data.MIMETYPE, SipAddress.SIP_ADDRESS, SipAddress.TYPE };
+    static private final String SELECT = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND (" + Contacts.HAS_PHONE_NUMBER + "=1) AND (" + Contacts.DISPLAY_NAME + " != '' ))";
+
+    private final Uri baseUri;
+    private final LongSparseArray<CallContact> filterFrom;
+    private volatile boolean abandon = false;
 
-    static private final String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND (" + Contacts.HAS_PHONE_NUMBER + "=1) AND (" + Contacts.DISPLAY_NAME + " != '' ))";
-    Uri baseUri;
+    public ContactsLoader(Context context) {
+        this(context, null, null);
+    }
 
-    public ContactsLoader(Context context, Uri u) {
+    public ContactsLoader(Context context, Uri base, LongSparseArray < CallContact > filter) {
         super(context);
-        baseUri = u;
+        baseUri = base;
+        filterFrom = filter;
+    }
+
+    private boolean checkCancel() {
+        return checkCancel(null);
+    }
+    private boolean checkCancel(Cursor c) {
+        if (isLoadInBackgroundCanceled()) {
+            Log.w(TAG, "Cancelled");
+            if (c != null)
+                c.close();
+            throw new OperationCanceledException();
+        }
+        if (abandon) {
+            Log.w(TAG, "Abandoned");
+            if (c != null)
+                c.close();
+            return true;
+        }
+        return false;
     }
 
     @Override
     public Result loadInBackground() {
-        Result res = new Result();
-
         ContentResolver cr = getContext().getContentResolver();
-        Cursor result = cr.query(baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
-        if (result == null)
-            return res;
-
-        int iID = result.getColumnIndex(Contacts._ID);
-        int iKey = result.getColumnIndex(ContactsContract.Data.LOOKUP_KEY);
-        int iName = result.getColumnIndex(Contacts.DISPLAY_NAME);
-        int iPhoto = result.getColumnIndex(Contacts.PHOTO_ID);
-        int iStarred = result.getColumnIndex(Contacts.STARRED);
-        CallContact.ContactBuilder builder = CallContact.ContactBuilder.getInstance();
-
-        while (result.moveToNext()) {
-            long cid = result.getLong(iID);
-            builder.startNewContact(cid, result.getString(iKey), result.getString(iName), result.getLong(iPhoto));
-            
-            Cursor cPhones = cr.query(Phone.CONTENT_URI, CONTACTS_PHONES_PROJECTION, Phone.CONTACT_ID + " =" + cid, null, null);
-            if (cPhones != null) {
-                while (cPhones.moveToNext()) {
-                    builder.addPhoneNumber(cPhones.getString(cPhones.getColumnIndex(Phone.NUMBER)), cPhones.getInt(cPhones.getColumnIndex(Phone.TYPE)));
-                    Log.w(TAG,"Phone:"+cPhones.getString(cPhones.getColumnIndex(Phone.NUMBER)));
-                }
-                cPhones.close();
-            }
 
-            //Cursor cSip = cr.query(Phone.CONTENT_URI, CONTACTS_SIP_PROJECTION, Phone.CONTACT_ID + "=" + cid, null, null);
-            Cursor cSip = cr.query(ContactsContract.Data.CONTENT_URI,
-                    CONTACTS_SIP_PROJECTION,
-                    ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?",
-                    new String[]{String.valueOf(cid), ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE}, null);
-            if (cSip != null) {
-                while (cSip.moveToNext()) {
-                    builder.addSipNumber(cSip.getString(cSip.getColumnIndex(SipAddress.SIP_ADDRESS)), cSip.getInt(cSip.getColumnIndex(SipAddress.TYPE)));
-                    Log.w(TAG, "SIP Phone for " + cid + " :" + cSip.getString(cSip.getColumnIndex(SipAddress.SIP_ADDRESS)));
+        long startTime = System.nanoTime();
+        final Result res = new Result();
+
+        if (baseUri != null) {
+            Cursor result = cr.query(baseUri, CONTACTS_ID_PROJECTION, SELECT, null, Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
+            if (result == null)
+                return res;
+
+            int iID = result.getColumnIndex(Contacts._ID);
+            long[] filter_ids = new long[result.getCount()];
+            int i = 0;
+            while (result.moveToNext()) {
+                long cid = result.getLong(iID);
+                filter_ids[i++] = cid;
+            }
+            result.close();
+            res.contacts.ensureCapacity(filter_ids.length);
+            int n = filter_ids.length;
+            for (i = 0; i < n; i++) {
+                CallContact c = filterFrom.get(filter_ids[i]);
+                res.contacts.add(c);
+                if (c.isStared())
+                    res.starred.add(c);
+            }
+        }
+        else {
+            StringBuilder cids = new StringBuilder();
+            LongSparseArray<CallContact> cache;
+            {
+                Cursor c = cr.query(ContactsContract.Data.CONTENT_URI, CONTACTS_SIP_PROJECTION,
+                        ContactsContract.Data.MIMETYPE + "=? OR " + ContactsContract.Data.MIMETYPE + "=?",
+                        new String[]{Phone.CONTENT_ITEM_TYPE, ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE}, null);
+                if (c != null) {
+
+                    cache = new LongSparseArray<>(c.getCount());
+                    cids.ensureCapacity(c.getCount() * 4);
+
+                    final int iID = c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID);
+                    final int iMime = c.getColumnIndex(ContactsContract.Data.MIMETYPE);
+                    final int iNumber = c.getColumnIndex(SipAddress.SIP_ADDRESS);
+                    final int iType = c.getColumnIndex(SipAddress.TYPE);
+                    while (c.moveToNext()) {
+                        long id = c.getLong(iID);
+                        CallContact contact = cache.get(id);
+                        if (contact == null) {
+                            contact = new CallContact(id);
+                            cache.put(id, contact);
+                            if (cids.length() > 0)
+                                cids.append(",");
+                            cids.append(id);
+                        }
+                        if (Phone.CONTENT_ITEM_TYPE.equals(c.getString(iMime))) {
+                            //Log.w(TAG, "Phone for " + id + " :" + cSip.getString(iNumber));
+                            contact.addPhoneNumber(c.getString(iNumber), c.getInt(iType));
+                        } else {
+                            //Log.w(TAG, "SIP Phone for " + id + " :" + cSip.getString(iNumber));
+                            contact.addNumber(c.getString(iNumber), c.getInt(iType), CallContact.NumberType.SIP);
+                        }
+                    }
+                    c.close();
+                } else {
+                    cache = new LongSparseArray<>();
                 }
-                cSip.close();
             }
-
-            res.contacts.add(builder.build());
-            if (result.getInt(iStarred) == 1) {
-                res.starred.add(builder.build());
+            if (checkCancel())
+                return null;
+            {
+                Cursor c = cr.query(Contacts.CONTENT_URI, CONTACTS_SUMMARY_PROJECTION,
+                        ContactsContract.Contacts._ID + " in (" + cids.toString() + ")", null,
+                        ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
+                if (c != null) {
+                    final int iID = c.getColumnIndex(Contacts._ID);
+                    final int iKey = c.getColumnIndex(ContactsContract.Data.LOOKUP_KEY);
+                    final int iName = c.getColumnIndex(Contacts.DISPLAY_NAME);
+                    final int iPhoto = c.getColumnIndex(Contacts.PHOTO_ID);
+                    final int iStarred = c.getColumnIndex(Contacts.STARRED);
+                    res.contacts.ensureCapacity(c.getCount());
+                    while (c.moveToNext()) {
+                        long id = c.getLong(iID);
+                        CallContact contact = cache.get(id);
+                        if (contact == null)
+                            Log.w(TAG, "Can't find contact with ID " + id);
+                        else {
+                            contact.setContactInfos(c.getString(iKey), c.getString(iName), c.getLong(iPhoto));
+                            res.contacts.add(contact);
+                            if (c.getInt(iStarred) != 0) {
+                                res.starred.add(contact);
+                                contact.setStared();
+                            }
+                        }
+                    }
+                    c.close();
+                }
             }
-           
         }
-        result.close();
 
-        return res;
+        long endTime = System.nanoTime();
+        long duration = (endTime - startTime) / 1000000;
+        Log.w(TAG, "Loading " + res.contacts.size() + " system contacts took " + duration / 1000. + "s");
+
+        return checkCancel() ? null : res;
+    }
+
+
+    @Override
+    protected void onAbandon() {
+        super.onAbandon();
+        abandon = true;
     }
 }
diff --git a/ring-android/app/src/main/java/cx/ring/service/LocalService.java b/ring-android/app/src/main/java/cx/ring/service/LocalService.java
index 02c22f2a6..540cd5b73 100644
--- a/ring-android/app/src/main/java/cx/ring/service/LocalService.java
+++ b/ring-android/app/src/main/java/cx/ring/service/LocalService.java
@@ -1,5 +1,26 @@
+/*
+ *  Copyright (C) 2015 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, write to the Free Software
+ *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
 package cx.ring.service;
 
+import android.Manifest;
 import android.app.Service;
 import android.content.AsyncTaskLoader;
 import android.content.BroadcastReceiver;
@@ -11,7 +32,10 @@ import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.Loader;
 import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
 import android.database.Cursor;
+import android.graphics.Bitmap;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
 import android.net.Uri;
@@ -19,10 +43,13 @@ import android.os.AsyncTask;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.provider.Contacts;
 import android.provider.ContactsContract;
 import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
 import android.util.Log;
 import android.util.LongSparseArray;
+import android.util.LruCache;
 import android.util.Pair;
 
 import java.lang.ref.WeakReference;
@@ -33,19 +60,22 @@ import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 import cx.ring.BuildConfig;
 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.SipCall;
+import cx.ring.model.SipUri;
 import cx.ring.model.TextMessage;
 import cx.ring.model.account.Account;
-import cx.ring.utils.Utilities;
 
 
 public class LocalService extends Service {
@@ -55,8 +85,10 @@ public class LocalService extends Service {
 
     public static final String AUTHORITY = "cx.ring";
     public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+    public static final int PERMISSIONS_REQUEST_READ_CONTACTS = 57;
 
     private ISipService mService = null;
+    private final ContactsContentObserver contactContentObserver = new ContactsContentObserver();
 
     // Binder given to clients
     private final IBinder mBinder = new LocalBinder();
@@ -68,7 +100,31 @@ public class LocalService extends Service {
 
     private HistoryManager historyManager;
 
-    AccountsLoader mAccountLoader = null;
+    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();
+
+    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 interface Callbacks {
         ISipService getRemoteService();
@@ -90,17 +146,39 @@ public class LocalService extends Service {
     public void onCreate() {
         Log.e(TAG, "onCreate");
         super.onCreate();
+
+        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, SipService.class);
         startService(intent);
         bindService(intent, mConnection, BIND_AUTO_CREATE | BIND_IMPORTANT | BIND_ABOVE_CLIENT );
     }
 
+    @Override
+    public void onLowMemory() {
+        super.onLowMemory();
+        mMemoryCache.evictAll();
+    }
+
     @Override
     public void onDestroy() {
         Log.e(TAG, "onDestroy");
         super.onDestroy();
         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>>() {
@@ -113,6 +191,19 @@ public class LocalService extends Service {
             sendBroadcast(new Intent(ACTION_ACCOUNT_UPDATE));
         }
     };
+    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);
+
+            sendBroadcast(new Intent(ACTION_ACCOUNT_UPDATE));
+        }
+    };
 
     private ServiceConnection mConnection = new ServiceConnection() {
         @Override
@@ -124,6 +215,12 @@ public class LocalService extends Service {
             mAccountLoader.registerListener(1, onAccountsLoaded);
             mAccountLoader.startLoading();
             mAccountLoader.forceLoad();
+
+            mSystemContactLoader = new ContactsLoader(LocalService.this);
+            mSystemContactLoader.registerListener(1, onSystemContactsLoaded);
+            mSystemContactLoader.startLoading();
+            mSystemContactLoader.forceLoad();
+
             startListener();
         }
 
@@ -134,7 +231,15 @@ public class LocalService extends Service {
                 mAccountLoader.unregisterListener(onAccountsLoaded);
                 mAccountLoader.cancelLoad();
                 mAccountLoader.stopLoading();
+                mAccountLoader = null;
+            }
+            if (mSystemContactLoader != null) {
+                mSystemContactLoader.unregisterListener(onSystemContactsLoaded);
+                mSystemContactLoader.cancelLoad();
+                mSystemContactLoader.stopLoading();
+                mSystemContactLoader = null;
             }
+
             //mBound = false;
             mService = null;
         }
@@ -166,6 +271,10 @@ public class LocalService extends Service {
         return super.onUnbind(intent);
     }
 
+    public static boolean checkContactPermissions(Context c) {
+        return ContextCompat.checkSelfPermission(c, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
+    }
+
     public ISipService getRemoteService() {
         return mService;
     }
@@ -213,7 +322,7 @@ public class LocalService extends Service {
         Log.w(TAG, "getByContact failed");
         return null;
     }
-    public Conversation getByCallId(String callId) {
+    public Conversation getConversationByCallId(String callId) {
         for (Conversation conv : conversations.values()) {
             Conference conf = conv.getConference(callId);
             if (conf != null)
@@ -223,6 +332,8 @@ public class LocalService extends Service {
     }
 
     public Conversation startConversation(CallContact contact) {
+        if (contact.isUnknown())
+            contact = findContactByNumber(CallContact.canonicalNumber(contact.getPhones().get(0).getNumber()));
         Conversation c = getByContact(contact);
         if (c == null) {
             c = new Conversation(contact);
@@ -240,33 +351,59 @@ public class LocalService extends Service {
     }
 
     public CallContact findContactById(long id) {
-        return findById(getContentResolver(), 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);
+            systemContactCache.put(id, c);
+        }
+        return c;
     }
 
     public Account guessAccount(CallContact c, String number) {
-        number = CallContact.canonicalNumber(number);
-        if (Utilities.isIpAddress(number))
+        SipUri uri = new SipUri(number);
+        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);
-        /*Conversation conv = getByContact(c);
-        if (conv != null) {
-            return
-        }*/
         return accounts.get(0);
     }
 
+    public void clearHistory() {
+        historyManager.clearDB();
+        new ConversationLoader(this, systemContactCache){
+            @Override
+            protected void onPostExecute(Map<String, Conversation> res) {
+                updated(res);
+            }
+        }.execute();
+    }
+
     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.PHOTO_THUMBNAIL_URI,
+            ContactsContract.Data.STARRED
     };
     public static final String[] CONTACT_PROJECTION = {
-            ContactsContract.Data._ID,
-            ContactsContract.Data.LOOKUP_KEY,
-            ContactsContract.Data.DISPLAY_NAME_PRIMARY,
-            ContactsContract.Data.PHOTO_ID,
+            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 = {
@@ -284,21 +421,15 @@ public class LocalService extends Service {
             ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS,
             ContactsContract.CommonDataKinds.SipAddress.TYPE
     };
-   /* private static final String[] CONTACTS_PHOTO_PROJECTION = {
-            ContactsContract.CommonDataKinds.Photo.,
-            ContactsContract.CommonDataKinds.SipAddress.TYPE
-    };
-*/
+
     private static final String ID_SELECTION = ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=?";
-    private static final String KEY_SELECTION = ContactsContract.Contacts.LOOKUP_KEY + "=?";
 
     private static void lookupDetails(@NonNull ContentResolver res, @NonNull CallContact c) {
         Log.w(TAG, "lookupDetails " + c.getKey());
 
         Cursor cPhones = res.query(
                 ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
-                CONTACTS_PHONES_PROJECTION,
-                ID_SELECTION,
+                CONTACTS_PHONES_PROJECTION, ID_SELECTION,
                 new String[]{String.valueOf(c.getId())}, null);
         if (cPhones != null) {
             final int iNum =  cPhones.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
@@ -325,27 +456,13 @@ public class LocalService extends Service {
             }
             cSip.close();
         }
-
-        /*Cursor cPhoto = res.query(targetUri,
-                CONTACTS_SIP_PROJECTION,
-                ContactsContract.Data.MIMETYPE + "=?",
-                new String[]{ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE}, null);
-        if (cSip != null) {
-            final int iSip =  cSip.getColumnIndex(ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS);
-            final int iType =  cSip.getColumnIndex(ContactsContract.CommonDataKinds.SipAddress.TYPE);
-            while (cSip.moveToNext()) {
-                c.addNumber(cSip.getString(iSip), cSip.getInt(iType), CallContact.NumberType.SIP);
-                Log.w(TAG, "SIP phone:" + cSip.getString(iSip));
-            }
-            cSip.close();
-        }*/
     }
 
     public static CallContact findByKey(@NonNull ContentResolver res, String key) {
         Log.e(TAG, "findByKey " + key);
 
         final CallContact.ContactBuilder builder = CallContact.ContactBuilder.getInstance();
-        Cursor result = res.query( Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, key), CONTACT_PROJECTION,
+        Cursor result = res.query(Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, key), CONTACT_PROJECTION,
                 null, null, null);
 
         CallContact contact = null;
@@ -354,6 +471,7 @@ public class LocalService extends Service {
             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.Data.STARRED);
             long cid = result.getLong(iID);
 
             Log.w(TAG, "Contact id:" + cid + " key:" + result.getString(iKey));
@@ -362,6 +480,8 @@ public class LocalService extends Service {
             result.close();
 
             contact = builder.build();
+            if (result.getInt(iStared) != 0)
+                contact.setStared();
             lookupDetails(res, contact);
         }
         return contact;
@@ -383,37 +503,33 @@ public class LocalService extends Service {
              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 id:" + cid + " key:" + result.getString(iKey));
 
              builder.startNewContact(cid, result.getString(iKey), result.getString(iName), result.getLong(iPhoto));
              contact = builder.build();
+             if (result.getInt(iStared) != 0)
+                 contact.setStared();
              lookupDetails(res, contact);
          }
          result.close();
          return contact;
     }
-    /*
-    public static int getRawContactId(@NonNull ContentResolver res, long contactId)
-    {
-        Cursor c= res.query(
-                ContactsContract.RawContacts.CONTENT_URI,
-                new String[]{ContactsContract.RawContacts._ID},
-                ContactsContract.RawContacts.CONTACT_ID+"=?",
-                new String[]{String.valueOf(contactId)},
-                null);
-        if (c == null)
-            return -1;
-        if (c.moveToFirst()) {
-            int rawContactId = c.getInt(c.getColumnIndex(ContactsContract.RawContacts._ID));
-            Log.d(TAG, "Contact Id: " + contactId + " Raw Contact Id: " + rawContactId);
-            return rawContactId;
-        }
-        c.close();
-        return -1;
-    }
-*/
+
+    public CallContact getContactById(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);
+        }*/
+        return c;
+    }
+
+
     @NonNull
     public static CallContact findContactBySipNumber(@NonNull ContentResolver res, String number) {
         final CallContact.ContactBuilder builder = CallContact.ContactBuilder.getInstance();
@@ -425,20 +541,20 @@ public class LocalService extends Service {
             Log.w(TAG, "findContactBySipNumber " + number + " can't find contact.");
             return CallContact.ContactBuilder.buildUnknownContact(number);
         }
-        int iID = result.getColumnIndex(ContactsContract.Data._ID);
         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);
 
         ArrayList<CallContact> contacts = new ArrayList<>(1);
         while (result.moveToNext()) {
             long cid = result.getLong(icID);
-            long id = result.getLong(iID);
             builder.startNewContact(cid, result.getString(iKey), result.getString(iName), result.getLong(iPhoto));
             CallContact contact = builder.build();
-            //Log.w(TAG, "findContactBySipNumber " + number + " found name:" + contact.getDisplayName() + " id:" + contact.getId() + " key:" + contact.getKey() + " rawid:"+getRawContactId(res, contact.getId()) + " rid:"+id + " photo:"+result.getLong(iPhoto) + " thumb:" + result.getString(iPhotoThumb));
+            if (result.getInt(iStared) != 0)
+                contact.setStared();
             lookupDetails(res, contact);
             contacts.add(contact);
         }
@@ -491,9 +607,11 @@ public class LocalService extends Service {
 
     private class ConversationLoader extends AsyncTask<Void, Void, Map<String, Conversation>> {
         private final ContentResolver cr;
+        private final LongSparseArray<CallContact> localContactCache;
 
-        public ConversationLoader(Context c) {
+        public ConversationLoader(Context c, LongSparseArray<CallContact> cache) {
             cr = c.getContentResolver();
+            localContactCache = (cache == null) ? new LongSparseArray<CallContact>(64) : cache;
         }
 
         private CallContact getByNumber(HashMap<String, CallContact> cache, String number) {
@@ -524,7 +642,6 @@ public class LocalService extends Service {
             List<HistoryText> historyTexts = null;
             Map<String, Conference> confs = null;
             final Map<String, Conversation> ret = new HashMap<>();
-            final LongSparseArray<CallContact> localContactCache = new LongSparseArray<>(64);
             final HashMap<String, CallContact> localNumberCache = new HashMap<>(64);
 
 
@@ -700,7 +817,6 @@ public class LocalService extends Service {
                     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());
@@ -710,7 +826,7 @@ public class LocalService extends Service {
         }
     }
 
-    final BroadcastReceiver receiver = new BroadcastReceiver() {
+    private final BroadcastReceiver receiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
             switch(intent.getAction()) {
@@ -739,11 +855,11 @@ public class LocalService extends Service {
                     mAccountLoader.startLoading();
                     break;
                 case CallManagerCallBack.INCOMING_TEXT:
-                case ConfigurationManagerCallback.INCOMING_TEXT:
+                case ConfigurationManagerCallback.INCOMING_TEXT: {
                     TextMessage txt = intent.getParcelableExtra("txt");
                     String call = txt.getCallId();
                     if (call != null && !call.isEmpty()) {
-                        Conversation conv = getByCallId(call);
+                        Conversation conv = getConversationByCallId(call);
                         conv.addTextMessage(txt);
                         /*Conference conf = conv.getConference(call);
                         conf.addSipMessage(txt);
@@ -757,9 +873,10 @@ public class LocalService extends Service {
                     }
                     sendBroadcast(new Intent(ACTION_CONF_UPDATE));
                     break;
+                }
                 default:
                     Log.w(TAG, "onReceive " + intent.getAction() + " " + intent.getDataString());
-                    new ConversationLoader(context){
+                    new ConversationLoader(context, systemContactCache){
                         @Override
                         protected void onPostExecute(Map<String, Conversation> res) {
                             updated(res);
@@ -771,13 +888,7 @@ public class LocalService extends Service {
 
     public void startListener() {
         final WeakReference<LocalService> self = new WeakReference<>(this);
-        new ConversationLoader(this){
-            @Override
-            protected void onPreExecute() {
-                super.onPreExecute();
-                Log.w(TAG, "onPreExecute");
-            }
-
+        new ConversationLoader(this, systemContactCache){
             @Override
             protected void onPostExecute(Map<String, Conversation> res) {
                 Log.w(TAG, "onPostExecute");
@@ -804,10 +915,28 @@ public class LocalService extends Service {
         intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
 
         registerReceiver(receiver, intentFilter);
+
+        getContentResolver().registerContentObserver(Contacts.People.CONTENT_URI, true, contactContentObserver);
+    }
+
+    private class ContactsContentObserver extends ContentObserver {
+
+        public ContactsContentObserver() {
+            super(null);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            Log.w(TAG, "ContactsContentObserver.onChange");
+            super.onChange(selfChange);
+            mSystemContactLoader.onContentChanged();
+            mSystemContactLoader.startLoading();
+        }
     }
 
     public void stopListener() {
         unregisterReceiver(receiver);
+        getContentResolver().unregisterContentObserver(contactContentObserver);
     }
 
 }
-- 
GitLab