I would like to list all contact groups in a Google account, and make it possible for a user to select some of the groups as "special". When an incoming number belongs to one of the "special" groups, I want to take actions.
I can't find too many examples around this. Has someone done anything similar that you would like to share?
You have three questions here:
How to enumerate the contact groups for a given account.
How to mark a group as special.
How to take action on contacts in that group.
So, going down the list...
1. Enumerating contact groups
The ContactsContract.Groups table stores the list of contact groups on the system. So, you'll want to issue a query that looks like this:
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri = ContactsContract.Groups.CONTENT_URI;
Log.i(TAG, "URI: " + uri);
String[] projection = new String[] {
ContactsContract.Groups._ID,
ContactsContract.Groups.TITLE
};
return new CursorLoader(this, uri, projection, null, null, null);
}
This loader will get you the list of all the groups on the system, and their database IDs.
How to mark a group as special
This is something your application will need to take care of. Just maintain a list of group IDs that are in your special list.
To determine whether a contact is in the "special" group, you can query the ContactsContract.Data table using a SQL where clause like the following:
String where = ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID
+ "="
+ groupid
+ " AND "
+ ContactsContract.CommonDataKinds.GroupMembership.MIMETYPE
+ "='"
+ ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE
+ "'";
where groupid is the database ID of the group you've marked as special. (If you have more than one group, start adding OR clauses.)
(You need to check for the CONTENT_ITEM_TYPE mimetype because the ContactsContract.Data table is used for storing arbitrary metadata for contacts, and the meaning of the columns in that table vary by mimetype. GroupMembership.GROUP_ROW_ID is simply a pointer to the column data1.)
3. How to take action on contacts in that group
This depends on what you're trying to accomplish, but in general, you'll create a broadcast receiver that listens for android.intent.action.PHONE_STATE. If the state in TelephonyManager.EXTRA_STATE is listed as TelephonyManager.EXTRA_STATE_RINGING, then you can get the phone number by looking in TelephonyManager.EXTRA_INCOMING_NUMBER to find the phone number.
(There's sample code for this attached to this article: http://www.krvarma.com/2010/08/detecting-incoming-and-outgoing-calls-in-android/)
You'll then need to check the Contacts database again to find any known contacts with that phone number. You can do this using the ContactsContract.PhoneLookup table.
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
resolver.query(uri, new String[]{PhoneLookup.DISPLAY_NAME,...
That will get you the Contact's database ID in the ContactsContract.Contacts table. The last step here is to match that up with the ContactsContract.RawContacts rows for that contact (search using the CONTACT_ID column), and find the set of RawContacts _IDs that represent that contact. You'll then use this to search through the group membership table, as described above.
I found way to get correct groups by all accounts in Android:
#Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
Uri uri = ContactsContract.Groups.CONTENT_SUMMARY_URI;
String[] projection = null;
//this is correct selection for retrieving groups from android
String selection = ContactsContract.Groups.ACCOUNT_TYPE + " NOT NULL AND " +
ContactsContract.Groups.ACCOUNT_NAME + " NOT NULL AND " + ContactsContract.Groups.DELETED + "=0";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
selection += " AND " + ContactsContract.Groups.AUTO_ADD + "=0 AND " + ContactsContract.Groups.FAVORITES + "=0";
}
String[] selectionArgs = null;
String sortOrder = ContactsContract.Groups.TITLE + " ASC";
Loader<Cursor> loader = null;
switch(loaderId) {
case GROUPS_LOADER:
loader = new CursorLoader(getActivity(), uri, projection, selection, selectionArgs, sortOrder);
break;
default:
break;
}
return loader;
}
source: grepcode.com
Related
I have something like this so far, but it seems like ContactsContract.Data URI returns multiple records with the same CONTACT_ID and LOOKUP_KEY. Is there any way to make this cursor return distinct records?
private static final Uri URI = ContactsContract.Data.CONTENT_URI;
#SuppressLint("InlinedApi")
private static final String[] PROJECTION = {
ContactsContract.Data._ID,
ContactsContract.Data.CONTACT_ID,
ContactsContract.Data.LOOKUP_KEY,
ContactsContract.Data.DISPLAY_NAME_PRIMARY
};
private static final String SELECTION =
ContactsContract.Data.DISPLAY_NAME_PRIMARY + " LIKE ?" +
" AND " + ContactsContract.Data.MIMETYPE + " = " + ContactsContract.CommonDataKinds.Organization.MIMETYPE +
" AND " + ContactsContract.CommonDataKinds.Organization.COMPANY + " LIKE ?";
private static final String SORT_ORDER =
ContactsContract.Data.DISPLAY_NAME_PRIMARY + " ASC";
#Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) {
String contactsFilter = getFilter(contactsSearch);
String companyFilter = getFilter(companySearch);
// Starts the query
return new CursorLoader(
getActivity(),
URI,
PROJECTION,
SELECTION,
new String[] { contactsFilter, companyFilter},
SORT_ORDER
);
}
Here's an example of the dump of this cursor:
42 {
_id=74752
contact_id=12603
lookup=2645ie9ffe868ace3d43
display_name=Person 1
}
43 {
_id=74753
contact_id=12603
lookup=2645ie9ffe868ace3d43
display_name=Person 1
}
44 {
_id=74756
contact_id=12603
lookup=2645ie9ffe868ace3d43
display_name=Person 1
}
_ID is different, but I want the cursor to return 1 record per person and all 3 of these are the same person.
Thoughts?
The Contacts DB is organized in three main tables:
Contacts - each entry represents one contact, and groups together one or more RawContacts
RawContacts - each entry represents data about a contact that was synced in by some SyncAdapter (e.g. Whatsapp, Google, Facebook, Viber), this groups multiple Data entries
Data - The actual data about a contact, emails, phones, etc. each line is a single piece of data that belongs to a single RawContact
Usually what happens is that an app (e.g. Google, Whatsapp, Linkedin) that wishes to create a new contact will create a new row in the RawContacts table that will usually contain just a name, and then use that row's _ID to add rows into the Data table for phones, emails, addresses, photos, etc.
You are querying over the Data table which means that if a certain contact has raw-contacts from 2 sources (e.g. 2 Google accounts) and both raw contacts contain a Data row with a certain company name, your cursor will return 2 results for that contact.
I would recommend not using CursorLoader here, instead create a regular Cursor from your projection/selection/selection-args, traverse it to get the data and then use a HashMap by contact-id (or some other method) to make the entries distinct by their Contact ID.
I'm working on an application in which I should be able to access all the contacts, able to update or delete the numbers in any contact.
I want to delete few numbers in a contact. I'm using batchOperations to perform delete operation.
phone = ContactsContract.Data.RAW_CONTACT_ID + " = ? AND " +
ContactsContract.Data.MIMETYPE + " = ? AND " +
ContactsContract.CommonDataKinds.Phone._ID + " = ?";
String[] phoneArgs = new String[]{Integer.toString(rawContactId), ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, String.valueOf(id)};
batchOperations.add(ContentProviderOperation.newDelete(Data.CONTENT_URI).withSelection(phone, phoneArgs).build());
this.mContext.getContentResolver().applyBatch("com.android.contacts", batchOperations);
batchOperations.clear();
I'm using this part of code to delete a specific number from the Contact. Using debugger, for a sample delete operation, I found the values as:
raw_contact_id = 4093
id = 21579
These values correspond to specific number("+814444444444") in a group of numbers in a sample contact.(Please refer image below)
But still the number is not getting deleted. I've been trying to figure it out for the last couple of hours but couldn't solve it. Please help.
Instead of supplying selection + selectionArgs, you can build a uri of the specific item you want to delete, like so:
Uri phoneUri = ContentUris.withAppendedId(Data.CONTENT_URI, phoneId);
batchOperations.add(ContentProviderOperation.newDelete(phoneUri).build());
A couple of other notes:
Always check the return value of applyBatch, if it's false, there's some issue with your code / permissions.
Use the constant ContactsContract.AUTHORITY instead of the hard-coded string "com.android.contacts".
Another reason for the observed behavior is that the phone "+814444444444" is stored on multiple RawContacts aggregated into a single contact.
In which case even when you properly delete the row from RawContact A, the contact profile would still get the number from RawContact B.
This is especially true for phone numbers when certain apps such as Whatsapp are installed which tend to copy over contacts' phone numbers to a separate RawContact under their own account.
If that's the issue you'll need to delete the phone number from ALL the contact's RawContacts holding that phone.
EDIT
Here's how to dump all phone numbers that belong to a specific contact, along with the raw-contact that holds them:
String selection = Data.CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?";
String[] selectionArgs = new String[] { contactId.toString(), Phone.CONTENT_ITEM_TYPE };
String[] projection = new String[] { Data.CONTACT_ID, Data.RAW_CONTACT_ID, Data.DATA1 };
Cursor cur = getContentResolver().query(Data.CONTENT_URI, projection, selection, selectionArgs, projection);
DatabaseUtils.dumpCursor(cur);
Also, note that a rawContactId is not an integer, it's a long.
I need a Cursor selecting contacts belonging to specific groups ordered by their family names (not display_names).
It is easy to get a cursor returning the contacts belonging to the requested groups, and another one returning the contacts sorted by family names.
However the family name belongs to DATA records with ContactsContract.Data.MIMETYPE = ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE while contacts belonging to specific groups are to be found in records with ContactsContract.Data.MIMETYPE = ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE.
How can I join records with different CONTENT_ITEM_TYPE but that share a common field, namely RAW_CONTACT_ID?
You said you need to select contacts, if so you shouldn't use RAW_CONTACT_ID, but instead CONTACT_ID to join your contacts data.
A single Contact may be an aggregate of multiple RawContacts and in that case I assume you want all the details of that single contacts as one row.
Now to get what you want, you can't use a Cursor to iterate through the contacts, instead you should load all the data you need to memory (e.g. HashMap) and run through that.
BTW, if you prefer to query over the Contacts/RawContacts tables instead of the Data table, you can utilise DISPLAY_NAME_ALTERNATIVE column to get your sort, see: https://developer.android.com/reference/android/provider/ContactsContract.ContactNameColumns.html#DISPLAY_NAME_ALTERNATIVE
Example code:
int selectedGroupId = 12345;
HashSet<Long> ids = new HashSet<>();
// get all CONTACT_IDs belonging to some GROUP_ID
String[] projection = new String[]{Data.CONTACT_ID};
String selection = Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE + "' AND " + GroupMembership.GROUP_ROW_ID + "=" + selectedGroupId;
Cursor c = getContentResolver().query(Data.CONTENT_URI, projection, selection, null, null);
while (c.moveToNext()) {
ids.add(c.getLong(0));
}
c.close();
String[] projection = new String[]{Data.DISPLAY_NAME, Data.MIMETYPE, Data.DATA1};
// you can add more MIMETYPES to the selection here to get phones, emails, etc. for each contact
String selection = Data.CONTACT_ID + " IN (" + TextUtils.join(",", ids) + ") AND " + Data.MIMETYPE + "='" + StructuredName.CONTENT_ITEM_TYPE + "'";
c = getContentResolver().query(Data.CONTENT_URI, projection, selection, null, StructuredName.FAMILY_NAME + " ASC");
DatabaseUtils.dumpCursor(c);
c.close();
I reference this link android developer training to implement for the retrieving contact detail with selection criteria
/*
* Defines the selection clause. Search for a lookup key
* and the Email MIME type
*/
private static final String SELECTION =
Data.LOOKUP_KEY + " = ?" +
" AND " +
Data.MIMETYPE + " = " +
"'" + Email.CONTENT_ITEM_TYPE + "'";
#Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) {
// Choose the proper action
switch (loaderId) {
case DETAILS_QUERY_ID:
// Assigns the selection parameter
mSelectionArgs[0] = mLookupKey;
// Starts the query
CursorLoader mLoader =
new CursorLoader(
getActivity(),
Data.CONTENT_URI,
PROJECTION,
SELECTION,
mSelectionArgs,
SORT_ORDER
);
...
}
When goes into change the contact name for example. Then in one case when adding contact is made by our application, cursor loader can't correctly detect the changes. I check the lookup key, that added contact key is different with others (for example : Orod-1340xxxxxxxx).
After searching the discussion, lookup key may change and suggest to use contact lookup uri with the lookup key. But the lookup uri cannot be used in above query. I need to query in Data table for the detail info.
How can i achieve that?
Thanks a lot.
Try using CONTACT_ID instead of LOOKUP_KEY.
private static final String SELECTION =
Data.CONTACT_ID + " = ? AND " +
Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'";
You might stumble upon warnings not to use contact-ids and instead use lookup keys or lookup uris, but that's for persisting a contact reference into a database for long keeping, if you're app is currently up and running, and just recently queried for this contact-id, it's perfectly safe and ok to use it, it's even better to use contact-ids for this purpose since it's more stable as a standalone id.
See more info here and here
update: looking at "vnd.android.cursor.dir/vnd.google.note" and "vnd.android.cursor.item/vnd.google.note" it seemed to me as though the cursor was for one table.
From the examples it appears as though content provider were designed to work with one table. I do know how to use multiple tables in sqlite but it seems to me that the content provider seems to be about picking one row or multiple rows from one table.
see http://developer.android.com/guide/topics/providers/content-provider-creating.html
Also, see the notepad sample in adt-bundle-windows-x86-20131030\sdk\samples\android-19\legacy\NotePad\src\com\example\android\notepad
Suppose I want to have notes by topic.
I would like to have a Topics table with columns _id and Title_text.
I would like to have the Notes table with columns _id and foreign key Topic_id and Note_text.
How would one design the Topics and Notes?
But looking at the Notes sample, the content URIs and docs on content providers, it appears as though having multiple related tables is an afterthought and is not obvious to me.
from NotepadProvider.java, Notepad.java:
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.google.note";
/**
* The MIME type of a {#link #CONTENT_URI} sub-directory of a single
* note.
*/
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.google.note";
public static final Uri CONTENT_ID_URI_BASE
= Uri.parse(SCHEME + AUTHORITY + PATH_NOTE_ID);
/**
* The content URI match pattern for a single note, specified by its ID. Use this to match
* incoming URIs or to construct an Intent.
*/
public static final Uri CONTENT_ID_URI_PATTERN
= Uri.parse(SCHEME + AUTHORITY + PATH_NOTE_ID + "/#");
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
...
switch (sUriMatcher.match(uri)) {
// If the incoming URI is for notes, chooses the Notes projection
case NOTES:
qb.setProjectionMap(sNotesProjectionMap);
break;
/* If the incoming URI is for a single note identified by its ID, chooses the
* note ID projection, and appends "_ID = <noteID>" to the where clause, so that
* it selects that single note
*/
case NOTE_ID:
qb.setProjectionMap(sNotesProjectionMap);
qb.appendWhere(
NotePad.Notes._ID + // the name of the ID column
"=" +
// the position of the note ID itself in the incoming URI
uri.getPathSegments().get(NotePad.Notes.NOTE_ID_PATH_POSITION));
break;
When creating a ContentProvider, the expectation is that other apps are going to use your database, and with that I mean other people who know nothing about your database scheme. To make things easy for them, you create and document your URIs:
To access all the books
content://org.example.bookprovider/books
to access books by id
content://org.example.bookprovider/books/#
to access books by author name
content://org.example.bookprovider/books/author
Create as many URIs as you need, that’s up to you. This way the user of your Provider can very easily access your database info, and maybe that’s why you are getting the impression that the Provider is designed to work with one table databases, but no, internally is where the work is done.
In your ContentProvider subclass, you can use a UriMatcher to identify those different URIs that are going to be passed to your ContentProvider methods (query, insert, update, delete). If the data the Uri is requesting is stored in several tables, you can actually do the JOINs and GROUP BYs or whatever you need with SQLiteQueryBuilder , e.g.
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder mQueryBuilder = new SQLiteQueryBuilder();
. . .
String Joins = " t1 INNER JOIN table2 t2 ON t2._id = t1._id"
+ " INNER JOIN table3 t3 ON t3._id = t1._id";
switch (mUriMatcher.match(uri)) {
case DATA_COLLECTION_URI:
mQueryBuilder.setTables(YourDataContract.TABLE1_NAME + Joins);
mQueryBuilder.setProjectionMap(. . .);
break;
case SINGLE_DATA_URI:
mQueryBuilder.setTables(YourDataContract.TABLE1_NAME + Joins);
mQueryBuilder.setProjectionMap(. . .);
mQueryBuilder.appendWhere(Table1._ID + "=" + uri.getPathSegments().get(1));
break;
case . . .
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
. . .
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
Cursor c = mQueryBuilder.query(db, projection, selection, selectionArgs, groupBy, having, orderBy);
return c;
}
Hope it helps.
Excuse me, but I don't understand your question.
ContentProvider is designed (a one of it's aims)to wrap access to your tabels. Design of database schema is up to you.
Generally, you need to:
Define your tables/ It should be made by execution of sql command in class which extends SQLiteOpenHelper
Define an uri for them
Define a logic for queries to this tables as it was made for NOTE_ID
Update
For JOIN operations SQLiteQueryBuilder is usually used. In setTables() you need to write names of tables with JOIN clause, e.g.
.setTables(NoteColumns.TABLENAME +
" LEFT OUTER JOIN " + TopicColumns.TABLENAME + " ON " +
NoteColumns.ID + " = " + TopicColumns.ID);
Here is my code for multiple table query in content provider with projectionMap
//HashMap for Projection
mGroupImageUri = new HashMap<>();
mGroupImageUri.put(RosterConstants.JID,RosterProvider.TABLE_ROSTER+"."+RosterConstants.JID);
mGroupImageUri.put(RosterConstants.USER_NAME,RosterProvider.TABLE_ROSTER+"."+RosterConstants.USER_NAME);
mGroupImageUri.put(ChatConstants.MESSAGE,"c."+ChatConstants.MESSAGE+ " AS "+ ChatConstants.MESSAGE);
mGroupImageUri.put(ChatConstants.SENDER,"c."+ChatConstants.SENDER+" AS "+ChatConstants.SENDER);
mGroupImageUri.put(ChatConstants.URL_LOCAL,"c."+ChatConstants.URL_LOCAL+" AS "+ChatConstants.URL_LOCAL);
//case for content type of uri
case IMAGE_URI:
qBuilder.setTables(RosterProvider.TABLE_ROSTER
+ " LEFT OUTER JOIN "+ TABLE_NAME + " c"
+ " ON c."+ ChatConstants.JID + "=" + RosterProvider.TABLE_ROSTER + "."+RosterConstants.JID);
qBuilder.setProjectionMap(mGroupImageUri);
break;
//ContentResolver query for Projection form, selection and selection args
String[] PROJECTION_FROM = new String[]{
RosterConstants.JID,
RosterConstants.USER_NAME,
ChatConstants.MESSAGE,
ChatConstants.SENDER,
ChatConstants.URL_LOCAL
};
String selection = RosterProvider.TABLE_ROSTER +"."+RosterConstants.JID+ "='" + jid + "' AND " + "c."+ChatConstants.FILE_TYPE+"="+ChatConstants.IMAGE;
String[] selectionArgu = null;
String order = "c."+ChatConstants.MESSAGE+" ASC";
Cursor cursor = mContentReolver.query(ChatProvider.CONTENT_URI_GROUP_IMAGE_URI,
PROJECTION_FROM,selection, null,order);
//#ChatProvider.CONTENT_URI_GROUP_IMAGE_URI = 'your content type uri'
//#TABLE_NAME = 'table1'
//#RosterProvider.TABLE_ROSTER ='table2'