package com.fsck.k9.activity.compose;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.content.AsyncTaskLoader;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Contacts.Data;
import android.support.annotation.Nullable;
import com.fsck.k9.R;
import com.fsck.k9.mail.Address;
import com.fsck.k9.view.RecipientSelectView.Recipient;
import com.fsck.k9.view.RecipientSelectView.RecipientCryptoStatus;
public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
/*
* Indexes of the fields in the projection. This must match the order in {@link #PROJECTION}.
*/
private static final int INDEX_NAME = 1;
private static final int INDEX_LOOKUP_KEY = 2;
private static final int INDEX_EMAIL = 3;
private static final int INDEX_EMAIL_TYPE = 4;
private static final int INDEX_EMAIL_CUSTOM_LABEL = 5;
private static final int INDEX_CONTACT_ID = 6;
private static final int INDEX_PHOTO_URI = 7;
private static final String[] PROJECTION = {
ContactsContract.CommonDataKinds.Email._ID,
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
ContactsContract.Contacts.LOOKUP_KEY,
ContactsContract.CommonDataKinds.Email.DATA,
ContactsContract.CommonDataKinds.Email.TYPE,
ContactsContract.CommonDataKinds.Email.LABEL,
ContactsContract.CommonDataKinds.Email.CONTACT_ID,
ContactsContract.Contacts.PHOTO_THUMBNAIL_URI
};
private static final String SORT_ORDER = "" +
ContactsContract.CommonDataKinds.Email.TIMES_CONTACTED + " DESC, " +
ContactsContract.Contacts.SORT_KEY_PRIMARY;
private static final int INDEX_EMAIL_ADDRESS = 0;
private static final int INDEX_EMAIL_STATUS = 1;
private static final String[] PROJECTION_NICKNAME = {
ContactsContract.Data.CONTACT_ID,
ContactsContract.CommonDataKinds.Nickname.NAME
};
private static final int INDEX_CONTACT_ID_FOR_NICKNAME = 0;
private static final int INDEX_NICKNAME = 1;
private static final String[] PROJECTION_CRYPTO_STATUS = {
"email_address",
"email_status"
};
private static final int CRYPTO_PROVIDER_STATUS_UNTRUSTED = 1;
private static final int CRYPTO_PROVIDER_STATUS_TRUSTED = 2;
private final String query;
private final Address[] addresses;
private final Uri contactUri;
private final Uri lookupKeyUri;
private final String cryptoProvider;
private List<Recipient> cachedRecipients;
private ForceLoadContentObserver observerContact, observerKey;
public RecipientLoader(Context context, String cryptoProvider, String query) {
super(context);
this.query = query;
this.lookupKeyUri = null;
this.addresses = null;
this.contactUri = null;
this.cryptoProvider = cryptoProvider;
}
public RecipientLoader(Context context, String cryptoProvider, Address... addresses) {
super(context);
this.query = null;
this.addresses = addresses;
this.contactUri = null;
this.cryptoProvider = cryptoProvider;
this.lookupKeyUri = null;
}
public RecipientLoader(Context context, String cryptoProvider, Uri contactUri, boolean isLookupKey) {
super(context);
this.query = null;
this.addresses = null;
this.contactUri = isLookupKey ? null : contactUri;
this.lookupKeyUri = isLookupKey ? contactUri : null;
this.cryptoProvider = cryptoProvider;
}
@Override
public List<Recipient> loadInBackground() {
List<Recipient> recipients = new ArrayList<>();
Map<String, Recipient> recipientMap = new HashMap<>();
if (addresses != null) {
fillContactDataFromAddresses(addresses, recipients, recipientMap);
} else if (contactUri != null) {
fillContactDataFromEmailContentUri(contactUri, recipients, recipientMap);
} else if (query != null) {
fillContactDataFromQuery(query, recipients, recipientMap);
} else if (lookupKeyUri != null) {
fillContactDataFromLookupKey(lookupKeyUri, recipients, recipientMap);
} else {
throw new IllegalStateException("loader must be initialized with query or list of addresses!");
}
if (recipients.isEmpty()) {
return recipients;
}
if (cryptoProvider != null) {
fillCryptoStatusData(recipientMap);
}
return recipients;
}
private void fillContactDataFromAddresses(Address[] addresses, List<Recipient> recipients,
Map<String, Recipient> recipientMap) {
for (Address address : addresses) {
// TODO actually query contacts - not sure if this is possible in a single query tho :(
Recipient recipient = new Recipient(address);
recipients.add(recipient);
recipientMap.put(address.getAddress(), recipient);
}
}
private void fillContactDataFromEmailContentUri(Uri contactUri, List<Recipient> recipients,
Map<String, Recipient> recipientMap) {
Cursor cursor = getContext().getContentResolver().query(contactUri, PROJECTION, null, null, null);
if (cursor == null) {
return;
}
fillContactDataFromCursor(cursor, recipients, recipientMap);
}
private void fillContactDataFromLookupKey(Uri lookupKeyUri, List<Recipient> recipients,
Map<String, Recipient> recipientMap) {
// We could use the contact id from the URI directly, but getting it from the lookup key is safer
Uri contactContentUri = Contacts.lookupContact(getContext().getContentResolver(), lookupKeyUri);
if (contactContentUri == null) {
return;
}
String contactIdStr = getContactIdFromContactUri(contactContentUri);
Cursor cursor = getContext().getContentResolver().query(
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
PROJECTION, ContactsContract.CommonDataKinds.Email.CONTACT_ID + "=?",
new String[] { contactIdStr }, null);
if (cursor == null) {
return;
}
fillContactDataFromCursor(cursor, recipients, recipientMap);
}
private static String getContactIdFromContactUri(Uri contactUri) {
return contactUri.getLastPathSegment();
}
private Cursor getNicknameCursor(String nickname) {
nickname = "%" + nickname + "%";
Uri queryUriForNickname = ContactsContract.Data.CONTENT_URI;
return getContext().getContentResolver().query(queryUriForNickname,
PROJECTION_NICKNAME,
ContactsContract.CommonDataKinds.Nickname.NAME + " LIKE ? AND " +
Data.MIMETYPE + " = ?",
new String[] { nickname, ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE },
null);
}
@SuppressWarnings("ConstantConditions")
private void fillContactDataFromQuery(String query, List<Recipient> recipients,
Map<String, Recipient> recipientMap) {
boolean foundValidCursor = false;
foundValidCursor |= fillContactDataFromNickname(query, recipients, recipientMap);
foundValidCursor |= fillContactDataFromNameAndEmail(query, recipients, recipientMap);
if (foundValidCursor) {
registerContentObserver();
}
}
private void registerContentObserver() {
if (observerContact != null) {
observerContact = new ForceLoadContentObserver();
getContext().getContentResolver().registerContentObserver(Email.CONTENT_URI, false, observerContact);
}
}
@SuppressWarnings("ConstantConditions")
private boolean fillContactDataFromNickname(String nickname, List<Recipient> recipients,
Map<String, Recipient> recipientMap) {
boolean hasContact = false;
final ContentResolver contentResolver = getContext().getContentResolver();
Uri queryUri = Email.CONTENT_URI;
Cursor nicknameCursor = getNicknameCursor(nickname);
if (nicknameCursor == null) {
return hasContact;
}
try {
while (nicknameCursor.moveToNext()) {
String id = nicknameCursor.getString(INDEX_CONTACT_ID_FOR_NICKNAME);
String selection = ContactsContract.Data.CONTACT_ID + " = ?";
Cursor cursor = contentResolver
.query(queryUri, PROJECTION, selection, new String[] { id }, SORT_ORDER);
String contactNickname = nicknameCursor.getString(INDEX_NICKNAME);
fillContactDataFromCursor(cursor, recipients, recipientMap, contactNickname);
hasContact = true;
}
} finally {
nicknameCursor.close();
}
return hasContact;
}
private boolean fillContactDataFromNameAndEmail(String query, List<Recipient> recipients,
Map<String, Recipient> recipientMap) {
ContentResolver contentResolver = getContext().getContentResolver();
query = "%" + query + "%";
Uri queryUri = Email.CONTENT_URI;
String selection = Contacts.DISPLAY_NAME_PRIMARY + " LIKE ? " +
" OR (" + Email.ADDRESS + " LIKE ? AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "')";
String[] selectionArgs = { query, query };
Cursor cursor = contentResolver.query(queryUri, PROJECTION, selection, selectionArgs, SORT_ORDER);
if (cursor == null) {
return false;
}
fillContactDataFromCursor(cursor, recipients, recipientMap);
return true;
}
private void fillContactDataFromCursor(Cursor cursor, List<Recipient> recipients,
Map<String, Recipient> recipientMap) {
fillContactDataFromCursor(cursor, recipients, recipientMap, null);
}
private void fillContactDataFromCursor(Cursor cursor, List<Recipient> recipients,
Map<String, Recipient> recipientMap, @Nullable String prefilledName) {
while (cursor.moveToNext()) {
String name = prefilledName != null ? prefilledName : cursor.getString(INDEX_NAME);
String email = cursor.getString(INDEX_EMAIL);
long contactId = cursor.getLong(INDEX_CONTACT_ID);
String lookupKey = cursor.getString(INDEX_LOOKUP_KEY);
// already exists? just skip then
if (recipientMap.containsKey(email)) {
// TODO merge? do something else? what do we do?
continue;
}
int addressType = cursor.getInt(INDEX_EMAIL_TYPE);
String addressLabel = null;
switch (addressType) {
case ContactsContract.CommonDataKinds.Email.TYPE_HOME: {
addressLabel = getContext().getString(R.string.address_type_home);
break;
}
case ContactsContract.CommonDataKinds.Email.TYPE_WORK: {
addressLabel = getContext().getString(R.string.address_type_work);
break;
}
case ContactsContract.CommonDataKinds.Email.TYPE_OTHER: {
addressLabel = getContext().getString(R.string.address_type_other);
break;
}
case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE: {
// mobile isn't listed as an option contacts app, but it has a constant so we better support it
addressLabel = getContext().getString(R.string.address_type_mobile);
break;
}
case ContactsContract.CommonDataKinds.Email.TYPE_CUSTOM: {
addressLabel = cursor.getString(INDEX_EMAIL_CUSTOM_LABEL);
break;
}
}
Recipient recipient = new Recipient(name, email, addressLabel, contactId, lookupKey);
if (recipient.isValidEmailAddress()) {
Uri photoUri = cursor.isNull(INDEX_PHOTO_URI) ? null : Uri.parse(cursor.getString(INDEX_PHOTO_URI));
recipient.photoThumbnailUri = photoUri;
recipientMap.put(email, recipient);
recipients.add(recipient);
}
}
cursor.close();
}
private void fillCryptoStatusData(Map<String, Recipient> recipientMap) {
List<String> recipientList = new ArrayList<>(recipientMap.keySet());
String[] recipientAddresses = recipientList.toArray(new String[recipientList.size()]);
Cursor cursor;
Uri queryUri = Uri.parse("content://" + cryptoProvider + ".provider.exported/email_status");
try {
cursor = getContext().getContentResolver().query(queryUri, PROJECTION_CRYPTO_STATUS, null,
recipientAddresses, null);
} catch (SecurityException e) {
// TODO escalate error to crypto status?
return;
}
initializeCryptoStatusForAllRecipients(recipientMap);
if (cursor == null) {
return;
}
while (cursor.moveToNext()) {
String email = cursor.getString(INDEX_EMAIL_ADDRESS);
int status = cursor.getInt(INDEX_EMAIL_STATUS);
for (Address address : Address.parseUnencoded(email)) {
String emailAddress = address.getAddress();
if (recipientMap.containsKey(emailAddress)) {
Recipient recipient = recipientMap.get(emailAddress);
switch (status) {
case CRYPTO_PROVIDER_STATUS_UNTRUSTED: {
if (recipient.getCryptoStatus() == RecipientCryptoStatus.UNAVAILABLE) {
recipient.setCryptoStatus(RecipientCryptoStatus.AVAILABLE_UNTRUSTED);
}
break;
}
case CRYPTO_PROVIDER_STATUS_TRUSTED: {
if (recipient.getCryptoStatus() != RecipientCryptoStatus.AVAILABLE_TRUSTED) {
recipient.setCryptoStatus(RecipientCryptoStatus.AVAILABLE_TRUSTED);
}
break;
}
}
}
}
}
cursor.close();
if (observerKey != null) {
observerKey = new ForceLoadContentObserver();
getContext().getContentResolver().registerContentObserver(queryUri, false, observerKey);
}
}
private void initializeCryptoStatusForAllRecipients(Map<String, Recipient> recipientMap) {
for (Recipient recipient : recipientMap.values()) {
recipient.setCryptoStatus(RecipientCryptoStatus.UNAVAILABLE);
}
}
@Override
public void deliverResult(List<Recipient> data) {
cachedRecipients = data;
if (isStarted()) {
super.deliverResult(data);
}
}
@Override
protected void onStartLoading() {
if (cachedRecipients != null) {
super.deliverResult(cachedRecipients);
return;
}
if (takeContentChanged() || cachedRecipients == null) {
forceLoad();
}
}
@Override
protected void onAbandon() {
super.onAbandon();
if (observerKey != null) {
getContext().getContentResolver().unregisterContentObserver(observerKey);
}
if (observerContact != null) {
getContext().getContentResolver().unregisterContentObserver(observerContact);
}
}
}