I'm trying to build a contacts-managing application. On my phone I have contacts from a couple of accounts including Facebook and HTC Facebook. For some reason I cannot retrieve these contacts from the RawContacts table of ContactsContract:
managedQuery(ContactsContract.RawContacts.CONTENT_URI, new String[] {
ContactsContract.RawContacts._ID,
ContactsContract.RawContacts.CONTACT_ID,
ContactsContract.RawContacts.ACCOUNT_NAME,
ContactsContract.RawContacts.ACCOUNT_TYPE,
}, ContactsContract.RawContacts.ACCOUNT_TYPE + " = 'com.facebook.auth.login'", null, null)
This query returns no results. If I repace the account type with com.htc.socialnetwork.facebook, I still get no results. There are many Facebook contacts on my phone; how to retrieve them?
I think I found the answer here: How to get a contact's facebook id or url from native contacts / content resolver?
Seems that access to Facebook contacts is restricted.
Yes, the Facebook contacts are secured, but can be hijacked with a little sneaky SQLite Injection. NO- I am not going to post here on this page, but is there any reason you do not just use Facebook authentication and get the contacts THAT way? The Facebook contacts don't really have anything useful in them anyway, and it is just more secure and more likely to work ALL the
There is definitely no way to do this in a standard way. So, we must use an SQLi injection (as roger commented) in order to be able to hack the contacts database and get the Facebook avatars. The following code works on most Motorolas, which use Motoblur, on Android 2.2 or higher:
public static Bitmap loadFacebookAvatar(Context context, long personId) {
String[] rawProjection = {ContactsContract.RawContacts._ID};
String contactIdAssertion = ContactsContract.RawContacts.CONTACT_ID + " = " + personId;
String rawWhere = new StringBuilder()
.append(contactIdAssertion).append(") UNION ALL SELECT ")
.append(ContactsContract.RawContacts._ID).append(" FROM view_raw_contacts WHERE (")
.append(contactIdAssertion).toString();
Cursor query = context.getContentResolver().query(ContactsContract.RawContacts.CONTENT_URI,
rawProjection,
rawWhere, null, null);
if (query != null && query.moveToFirst()) {
do {
long id = query.getLong(query.getColumnIndex(ContactsContract.RawContacts._ID));
String[] projection = {ContactsContract.CommonDataKinds.Photo.PHOTO};
Uri uri = ContactsContract.Data.CONTENT_URI;
String mimeTypeAssertion = ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE + "'";
String photoAssertion = ContactsContract.CommonDataKinds.Photo.PHOTO + " IS NOT NULL";
String rawContactIdAssertion = ContactsContract.CommonDataKinds.Photo.RAW_CONTACT_ID + " = " + id;
String where = new StringBuilder().append(mimeTypeAssertion).append(" AND ")
.append(photoAssertion).append(" AND ").append(rawContactIdAssertion)
.append(") UNION ALL SELECT ").append(ContactsContract.CommonDataKinds.Photo.PHOTO)
.append(" FROM view_data WHERE (").append(photoAssertion).append(" AND ")
.append(rawContactIdAssertion).toString();
Cursor photoQuery = context.getContentResolver().query(uri, projection, where, null, null);
if (photoQuery != null && photoQuery.moveToFirst()) {
do {
byte[] photoData = photoQuery.getBlob(photoQuery.getColumnIndex(ContactsContract.CommonDataKinds.Photo.PHOTO));
if (photoData != null) {
return BitmapFactory.decodeByteArray(photoData, 0, photoData.length, null);
}
} while (photoQuery.moveToNext());
}
} while (query.moveToNext());
}
return null;
}
For other handsets you must get the contacts database and analyze it in order to determine how to apply the SQL Injection, which requires a rooted phone.
Related
Background
Contacts on the address book can have an account data that's attached to them.
Each app can have an account, and then add its own information for the contact.
Apps such as Telegram, WhatsApp, Viber,... - all create an account that adds information and/or actions to contacts.
Here's an example of a contact that has both WhatsApp and Viber accounts for it:
The problem
I'm trying to figure out how to fetch all contacts that have a specified account.
Since WhatsApp is the most popular that I know of, my tests focus on it.
My problem is that some users claim what I did barely returns contacts, and some claim it doesn't show even a single one. It seems to usually work, and in my case it always worked, but something is probably not good on the code.
What I've tried
I got to make the next code, which to me seems to work, getting a map of phone-to-contact-info, of all WhatsApp contacts.
The idea is to get all possible information of WhatsApp contacts, vs all basic contacts data, and merge those that match the same lookup-key.
I tried to use a better query of joining, but I failed. Maybe it is possible too, and might be more efficient.
Here's the code:
/**
* returns a map of lookup-key to contact-info, of all WhatsApp contacts
*/
#NonNull
public HashMap<String, ContactInfo> getAllWhatsAppPhones(Context context) {
ContentResolver cr = context.getContentResolver();
final HashMap<String, ContactInfo> phoneToContactInfoMap = new HashMap<>();
final HashMap<String, String> whatsAppLookupKeyToPhoneMap = new HashMap<>();
final String phoneMimeType = Phone.CONTENT_ITEM_TYPE;
final Cursor whatsAppCursor;
whatsAppCursor = cr.query(Data.CONTENT_URI,
new String[]{Phone.NUMBER, Phone.LOOKUP_KEY},
Phone.MIMETYPE + " = ?", new String[]{WhatsAppStuff.WHATS_APP_MIME_TYPE}, null);
if (whatsAppCursor == null)
return phoneToContactInfoMap;
Cursor contactCursor = cr.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
//null,
new String[]{
Contacts.LOOKUP_KEY, Contacts._ID, Contacts.PHOTO_THUMBNAIL_URI,
ContactsContract.Contacts.DISPLAY_NAME, // ContactsContract.CommonDataKinds.Phone.NUMBER,
},
"(" + Phone.MIMETYPE + " IS NULL OR " + Phone.MIMETYPE + " = '" + phoneMimeType + "') AND ("
+ ContactsContract.RawContacts.ACCOUNT_TYPE + " = 'com.google' OR " + ContactsContract.RawContacts.ACCOUNT_TYPE + " IS NULL)",
null, null);
if (contactCursor == null) {
whatsAppCursor.close();
return phoneToContactInfoMap;
}
int progress = 0;
final int phoneNumberIdx = whatsAppCursor.getColumnIndex(Phone.NUMBER);
final int lookupKeyIdx = whatsAppCursor.getColumnIndex(Phone.LOOKUP_KEY);
while (whatsAppCursor.moveToNext()) {
final String phoneNumberValue = whatsAppCursor.getString(phoneNumberIdx);
final int endIndex = phoneNumberValue.indexOf("#");
if (endIndex < 0)
continue;
String lookupKey = whatsAppCursor.getString(lookupKeyIdx);
final String phone = phoneNumberValue.substring(0, endIndex);
if (!phone.isEmpty() && StringUtil.isAllDigits(phone)) {
//Log.d("AppLog", "whatsApp phone:" + phone + " " + lookupKey);
whatsAppLookupKeyToPhoneMap.put(lookupKey, phone);
}
if (markedToCancel != null && markedToCancel.get()) {
whatsAppCursor.close();
contactCursor.close();
return phoneToContactInfoMap;
}
if (progressListener != null)
progressListener.onProgressUpdate(progress++, maxProgress);
}
whatsAppCursor.close();
if (whatsAppLookupKeyToPhoneMap.isEmpty())
return phoneToContactInfoMap;
//Log.d("AppLog", "getting info about whatsapp contacts");
final int idColIdx = contactCursor.getColumnIndex(Contacts._ID);
final int displayNameColIdx = contactCursor.getColumnIndex(Contacts.DISPLAY_NAME);
final int lookupKeyColIdx = contactCursor.getColumnIndex(Contacts.LOOKUP_KEY);
final int photoColIdx = contactCursor.getColumnIndex(Contacts.PHOTO_THUMBNAIL_URI);
while (contactCursor.moveToNext()) {
String lookupKey = contactCursor.getString(lookupKeyColIdx);
String phoneNumber = whatsAppLookupKeyToPhoneMap.get(lookupKey);
if (phoneNumber == null)
continue;
ContactInfo contactInfo = new ContactInfo();
contactInfo.lookupKey = lookupKey;
contactInfo.displayName = contactCursor.getString(displayNameColIdx);
contactInfo.photoThumbUriStr = contactCursor.getString(photoColIdx);
contactInfo.whatsAppPhoneNumber = phoneNumber;
contactInfo.contactId = contactCursor.getLong(idColIdx);
phoneToContactInfoMap.put(phoneNumber, contactInfo);
if (markedToCancel != null && markedToCancel.get()) {
contactCursor.close();
return phoneToContactInfoMap;
}
if (progressListener != null)
progressListener.onProgressUpdate(progress++, maxProgress);
}
contactCursor.close();
return phoneToContactInfoMap;
}
The question
As I wrote, the above code only usually works.
How come it only usually works? What's missing to fix it?
Should I use Contacts.getLookupUri instead of lookup key? If so, how should I change the code above to use it instead?
I tried to use a URI instead of a lookup-key, but then it didn't find any of them inside the normal contacts.
The main issue I see that can explain why users won't see results from your code, is that you're assuming all the contacts are stored on a Google account.
While this is the default behavior in some devices, it's not the default on all devices, also, users can freely change their contacts storage to any other location (yahoo contacts, MS exchange, phone-only (unsynced), etc.)
Having that said, if your only requirement is to
fetch all contacts that have a specified account.
I think that's a much better alternative then your 2 queries (one of which runs over all contacts, not just the required ones):
// Uri to query contacts that have a RawContact in the desired account
final Uri.Builder builder = Contacts.CONTENT_URI.buildUpon();
builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, whatsappAccountName);
builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, whatsappAccountType);
Uri uri = builder.build();
String[] projection = new String[]{ Contacts.LOOKUP_KEY, Contacts._ID Contacts.DISPLAY_NAME }; // add more if needed
// boo-yaa!
Cursor cur = cr.query(uri, projection, null, null, null);
I am very new to app development. I am trying to read contact info without having to request permission to contacts (so I am using intents).
I get a URI with the following code in my main activity:
Intent selectContactIntent = new Intent(Intent.ACTION_PICK);
selectContactIntent.setType(ContactsContract.Contacts.CONTENT_TYPE);
if (selectContactIntent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(selectContactIntent, REQUEST_CODE_SELECT_CONTACT);
}
else {
showContactRequiredMessage(view);
}
In another (sub)activity, I do the following:
contactUri = intent.getParcelableExtra(MainActivity.CONTACT_URI);
String[] projection = new String[] {
ContactsContract.Contacts.Data._ID,
ContactsContract.Contacts.Data.MIMETYPE,
ContactsContract.Contacts.Data.DATA1,
ContactsContract.Contacts.Data.DATA2,
ContactsContract.Contacts.Data.DATA3,
ContactsContract.Contacts.Data.DATA4,
ContactsContract.Contacts.Data.DATA5,
ContactsContract.Contacts.Data.DATA6,
ContactsContract.Contacts.Data.DATA7,
ContactsContract.Contacts.Data.DATA8,
ContactsContract.Contacts.Data.DATA9,
ContactsContract.Contacts.Data.DATA10,
ContactsContract.Contacts.Data.DATA11,
ContactsContract.Contacts.Data.DATA12,
ContactsContract.Contacts.Data.DATA13,
ContactsContract.Contacts.Data.DATA14,
ContactsContract.Contacts.Data.DATA15
};
Cursor contactResults = getContentResolver().query(contactUri, projection, null, null, null);
The last line throws the exception java.lang.IllegalArgumentException: Invalid column <any column after _ID>
My app doesn't require all of the data in reality I just want to see what is available, I will most likely need first name, last name, phone, and email.
My issue is the MIME type that I set on the intent when I request the contact info. The documentation states ContactsContract.Contacts.CONTENT_TYPE should be used. However, if I use, something like ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE, I can get display name and phone number. I am not sure if this means I will need to make multiple queries to get everything (the information shown in the contact picker changes when changing the type requested).
TL;DR: Used the "wrong" content type when creating the intent to select a contact.
As mentioned in my comment to your answer, you should be able to get the information expected without using a specific CONTENT_TYPE like CommonDataKinds.Phone.CONTENT_TYPE.
The problem I see in your code is that you're trying to access Data table info from a Contacts table uri.
The ContactsContract api stored info on 3 main tables: Contacts, RawContacts and Data.
You were given a contactUri which points to an entry in the Contacts table, use the following code to read Data entries related to that contact:
long contactId = ContentUris.parseId(contactUri);
String projection = String[] { Data.MIMETYPE, Data.DISPLAY_NAME, Data.DATA1 };
String selection = Data.CONTACT_ID + " = " + contactId;
Cursor cursor = getContentResolver().query(Data.CONTENT_URI, projection, selection, null, null);
while (cursor != null && cursor.moveToNext()) {
String mime = cursor.getString(0);
String name = cursor.getString(1);
String info = cursor.getString(2);
if (mime.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
Log.d(TAG, name + ": email = " + info;
}
if (mime.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
Log.d(TAG, name + ": phone = " + info;
}
// Add more mimetypes here if needed...
}
if (cursor != null) {
cursor.close();
}
I am new to Android, currently I can retrieve contact list and image from Google contact. However, the contact list from my App does not provide suggestion for email within my company like the Google Gmail app.
Is it possible that I can retrieve these extra email addresses?
Below is the code to retrieve contact
public ArrayList<String> getNameEmailDetails() {
ArrayList<String> emlRecs = new ArrayList<String>();
HashSet<String> emlRecsHS = new HashSet<String>();
ContentResolver cr = getContentResolver();
String[] PROJECTION = new String[] { ContactsContract.RawContacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.PHOTO_ID,
ContactsContract.CommonDataKinds.Email.DATA,
ContactsContract.CommonDataKinds.Photo.CONTACT_ID };
String order = "CASE WHEN "
+ ContactsContract.Contacts.DISPLAY_NAME
+ " NOT LIKE '%#%' THEN 1 ELSE 2 END, "
+ ContactsContract.Contacts.DISPLAY_NAME
+ ", "
+ ContactsContract.CommonDataKinds.Email.DATA
+ " COLLATE NOCASE";
String filter = ContactsContract.CommonDataKinds.Email.DATA + " NOT LIKE ''";
Cursor cur = cr.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, PROJECTION, filter, null, order);
if (cur.moveToFirst()) {
do {
// names comes in hand sometimes
String name = cur.getString(1);
String email = cur.getString(3);
String contact_id = cur.getString(4);
Bitmap profilePic = openPhoto(Long.parseLong(contact_id));
if (profilePic != null) {
profileList.add(new WordMatchAdapterWithIMG.profileWithIMG(email, profilePic));
}
} while (cur.moveToNext());
}
cur.close();
return emlRecs;
}
I would suggest you use the Google Contacts API.
The Google Contacts API allows client applications to view and update a user's contacts. Contacts are stored in the user's Google Account; most Google services have access to the contact list.
To retrieve a single contact, send an authorized GET request to the contact's selfLink URL:
https://www.google.com/m8/feeds/contacts/{userEmail}/full/{contactId}
Upon success, the server responds with an HTTP 200 OK status code and the requested contact entry.
public static ContactEntry retrieveContact(ContactsService myService) {
ContactEntry contact =
myService.getEntry(new URL("https://www.google.com/m8/feeds/contacts/default/full/contactId"),
ContactEntry.class);
// Do something with the contact.
return contact;
}
Useful Materials
Fetch Google/Gmail contacts using Google Contacts API and OAuth2 in Android
PHP GMAIL Contacts XML Parsing with DOMDocument and cURL
The short version of my question is: How do I access the phone numbers of contacts that were synced from 3rd party apps?
Here is the long version:
I can access the regular Android contacts pretty easily. The issue is when the only information in the contact list is synced with a 3rd party app like Facebook or LinkedIn. If I physically went and typed someone's phone number into the Google Contacts List, everything works fine.
However, if this phone number came from syncing my facebook account to my contact list, no phone number shows up, even though if I navigated to Google's pre-made contact list I can see a phone number is actually attached to the contact. Here is the code I use for getting the phone numbers.
public void populateNumberLists(View view)
{
LinearLayout ll = (LinearLayout) view;
TextView tv = (TextView) ll.findViewById(R.id.contactEntryText);
String str = (String) tv.getText();
Cursor cursor = getNumbers(str);
String[] fields = new String[] {
cursor.getColumnName(1).toString()
};
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.numberentry, cursor,
fields, new int[] {R.id.numberEntryText});
mNumberList.setAdapter(adapter);
}
private Cursor getNumbers(String str)
{
final Uri URIs = ContactsContract.Contacts.CONTENT_URI;
final String ID = ContactsContract.Contacts.LOOKUP_KEY;
String id = "";
ContentResolver cr = getContentResolver();
Cursor cu = cr.query(URIs, null, ContactsContract.Contacts.DISPLAY_NAME + " = '" + str + "'", null, null);
if (cu.moveToFirst()) {
id = cu.getString(cu.getColumnIndex(ID));
}
cu.close();
// Run query
Uri uri = Phone.CONTENT_URI;
String[] projection = new String[] {
Phone._ID,
Phone.NUMBER
};
String selection = Phone.LOOKUP_KEY + " = '" + id + "' and (" + Phone.TYPE + " = '" + Phone.TYPE_HOME+"' or " + Phone.TYPE + " = '" + Phone.TYPE_MOBILE+"' or " + Phone.TYPE + " = '" + Phone.TYPE_WORK+"' or " + Phone.TYPE + " = '" + Phone.TYPE_WORK_MOBILE+"')";
String[] selectionArgs = null;
String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
return managedQuery(uri, projection, selection, selectionArgs, null);
}
Basically, the populateNumberLists function takes a clicked item from a list view, determines which contact from the list was clicked and calls the function getNumbers.
The getNumbers function takes actual name that was clicked, gets the lookup key for that name, then grabs all the phone numbers associated with that lookup key.
Oh, related to this, the only names displayed in the contact list are ones where ContactsContract.Contacts.HAS_PHONE_NUMBER equals 1. So I know that all the contacts that can be selected have a phone number attached.
Facebook is not included in the ContactPicker because Facebook forbid that.
This is a politically thing and won't be solved soon: Google wants Facebook to share data, Facebook uses Google but doesn't share..
You'll have to use the Facebook SDK for android to do this. Use an FQL query to get the phone number.
I want to get the nickname of a contact from addressbook. I start with his phone number, query it and want the nickname (aka alias) as a result.
Cursor cur = context.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, ContactsContract.CommonDataKinds.Phone.NUMBER + " = " + incomingNumber, null, null);
if (cur.moveToFirst()) {
Log.e("saymyname", cur.getString(cur.getColumnIndex(ContactsContract.CommonDataKinds.Nickname.NAME)));
Log.e("saymyname", cur.getString(cur.getColumnIndex(ContactsContract.CommonDataKinds.Nickname.LABEL)));
}
Output of the logs is the incomingNumber (first Log.e() ) and null (second Log.e() ), but I want to get the contact's nickname!
Thanks
Tom
Nickname is held in a different table than the phone numbers, you have to query ContactsContract.Data.CONTENT_URI
Check my answer on this question
(I don't have necessary reputation to comment so I have to add answer)
TomTasche's answer is misleading and it got me to waste a lot of time trying to figure out why I couldn't get the proper nickname on a contact I knew had one.
I found the answer by myself but got the confirmation from this post that I was now doing it properly.
Basically when you read the ContactsContract.Data documentation you read:
Each row of the data table is typically used to store a single piece of contact information (such as a phone number) and its associated metadata (such as whether it is a work or home number).
That explains the shady part of TomTasche's code :
if (nickname.equals(incomingNumber)) {
return name;
}
Since doing a search with just the CONTACT_ID can return multiple rows (one for each information type), it's not guaranteed that the first one contains the nickname. When there is a nickname it will be in DATA1, but the phone number is also found in DATA1.
The example part in the documentation of ContactsContract.Data makes it clear that rows must be selected based on their MIME_TYPE, only when the MIME_TYPE is selected, can we start making sense of the content in the row itself.
A proper query would therefore be:
cursor = getContentResolver().query(
ContactsContract.Data.CONTENT_URI,
new String[]{Nickname.NAME},
ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + "= ?",
new String[]{contactID, Nickname.CONTENT_ITEM_TYPE},
null);
(where Nickname is ContactsContract.CommonDataKinds.Nickname)
I felt I had to say something on this topic to prevent other people wasting as much time as I did based on this sole topic (first one I found with my Google friend).
The answer from Pentium10 was very helpful! Thanks!
If anybody needs a sample, look at the following code:
public String accessContact(String incomingNumber) {
if (incomingNumber == null || "".equals(incomingNumber)) {
return "unknown";
}
try {
Cursor cur = context.getContentResolver().query(Phone.CONTENT_URI, new String[] {Phone.DISPLAY_NAME, Phone.TYPE, Phone.CONTACT_ID}, Phone.NUMBER + " = " + incomingNumber, null, null);
int nameIndex = cur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
int typeIndex = cur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE);
int idIndex = cur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID);
String name;
String type;
String id;
if (cur.moveToFirst()) {
name = cur.getString(nameIndex);
type = cur.getString(typeIndex);
id = cur.getString(idIndex);
} else {
return "unknown";
}
cur = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, new String[] {ContactsContract.Data.DATA1}, ContactsContract.Data.CONTACT_ID + " = " + id, null, null);
int nicknameIndex = cur.getColumnIndex(ContactsContract.Data.DATA1);
String nickname;
if (cur.moveToFirst()) {
nickname = cur.getString(nicknameIndex);
if (nickname.equals(incomingNumber)) {
return name;
}
return nickname;
} else {
return name;
}
} catch (Exception e) {
e.printStackTrace();
return "unknown";
}