Skip to content
Snippets Groups Projects
Commit f6146f83 authored by Adrien Béraud's avatar Adrien Béraud
Browse files

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
parent 16b8b44c
No related branches found
No related tags found
No related merge requests found
......@@ -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();
......
......@@ -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;
}
......
......@@ -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() {
......
......@@ -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;
}
......
/*
* 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;
}
}
/*
* 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);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment