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 1754589c021294088a91e63ebf137218fe8fb480..231e05efb81532aaf10862c8cfbd3393d314e91d 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 1e70751285b07ececf089d7c055897ffdf303eda..c0c0a69db214b3aee17fdc93ff95de1da71bb5ae 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 07d33fd385586fe7ca3cbd1f2b5fdce8c44360d8..02ce5e7af762769b11eec9adc74192b1b0750e74 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 a2d8a57a435ce7bdd6dd56329540fe6cdd53ce59..05a663e1c757875f86d964d54bc0d6f26ce2bb58 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 fd71e80af58d10ada498de2f7312d37fb1d234d9..961f4e5f02fe65409c0193c994d8d27ec6f48f12 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 02c22f2a6f1c16e5634e52c2d3b263ed14566de5..540cd5b7377121a9f68da0516df84dc21bec581c 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); } }