diff --git a/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientLoader.java b/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientLoader.java index 7036ece84..cbe6c31e0 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientLoader.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientLoader.java @@ -21,6 +21,7 @@ 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; +import timber.log.Timber; public class RecipientLoader extends AsyncTaskLoader> { @@ -58,6 +59,13 @@ public class RecipientLoader extends AsyncTaskLoader> { private static final int INDEX_CONTACT_ID_FOR_NICKNAME = 0; private static final int INDEX_NICKNAME = 1; + private static final String[] PROJECTION_CRYPTO_ADDRESSES = { + "address", + "uid_address" + }; + + private static final int INDEX_USER_ID = 1; + private static final String[] PROJECTION_CRYPTO_STATUS = { "address", "uid_key_status", @@ -77,6 +85,7 @@ public class RecipientLoader extends AsyncTaskLoader> { private final Uri contactUri; private final Uri lookupKeyUri; private final String cryptoProvider; + private final ContentResolver contentResolver; private List cachedRecipients; private ForceLoadContentObserver observerContact, observerKey; @@ -89,6 +98,8 @@ public class RecipientLoader extends AsyncTaskLoader> { this.addresses = null; this.contactUri = null; this.cryptoProvider = cryptoProvider; + + contentResolver = context.getContentResolver(); } public RecipientLoader(Context context, String cryptoProvider, Address... addresses) { @@ -98,6 +109,8 @@ public class RecipientLoader extends AsyncTaskLoader> { this.contactUri = null; this.cryptoProvider = cryptoProvider; this.lookupKeyUri = null; + + contentResolver = context.getContentResolver(); } public RecipientLoader(Context context, String cryptoProvider, Uri contactUri, boolean isLookupKey) { @@ -107,6 +120,8 @@ public class RecipientLoader extends AsyncTaskLoader> { this.contactUri = isLookupKey ? null : contactUri; this.lookupKeyUri = isLookupKey ? contactUri : null; this.cryptoProvider = cryptoProvider; + + contentResolver = context.getContentResolver(); } @Override @@ -120,6 +135,10 @@ public class RecipientLoader extends AsyncTaskLoader> { fillContactDataFromEmailContentUri(contactUri, recipients, recipientMap); } else if (query != null) { fillContactDataFromQuery(query, recipients, recipientMap); + + if (cryptoProvider != null) { + fillContactDataFromCryptoProvider(query, recipients, recipientMap); + } } else if (lookupKeyUri != null) { fillContactDataFromLookupKey(lookupKeyUri, recipients, recipientMap); } else { @@ -137,6 +156,40 @@ public class RecipientLoader extends AsyncTaskLoader> { return recipients; } + private void fillContactDataFromCryptoProvider(String query, List recipients, + Map recipientMap) { + Cursor cursor; + try { + Uri queryUri = Uri.parse("content://" + cryptoProvider + ".provider.exported/autocrypt_status"); + cursor = contentResolver.query(queryUri, PROJECTION_CRYPTO_ADDRESSES, null, + new String[] { "%" + query + "%" }, null); + + if (cursor == null) { + return; + } + } catch (SecurityException e) { + Timber.e(e, "Couldn't obtain recipients from crypto provider!"); + return; + } + + while (cursor.moveToNext()) { + String uid = cursor.getString(INDEX_USER_ID); + Address[] addresses = Address.parseUnencoded(uid); + + for (Address address : addresses) { + if (recipientMap.containsKey(address.getAddress())) { + continue; + } + + Recipient recipient = new Recipient(address); + recipients.add(recipient); + recipientMap.put(address.getAddress(), recipient); + } + } + + cursor.close(); + } + private void fillContactDataFromAddresses(Address[] addresses, List recipients, Map recipientMap) { for (Address address : addresses) { @@ -149,7 +202,7 @@ public class RecipientLoader extends AsyncTaskLoader> { private void fillContactDataFromEmailContentUri(Uri contactUri, List recipients, Map recipientMap) { - Cursor cursor = getContext().getContentResolver().query(contactUri, PROJECTION, null, null, null); + Cursor cursor = contentResolver.query(contactUri, PROJECTION, null, null, null); if (cursor == null) { return; @@ -161,14 +214,14 @@ public class RecipientLoader extends AsyncTaskLoader> { private void fillContactDataFromLookupKey(Uri lookupKeyUri, List recipients, Map 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); + Uri contactContentUri = Contacts.lookupContact(contentResolver, lookupKeyUri); if (contactContentUri == null) { return; } String contactIdStr = getContactIdFromContactUri(contactContentUri); - Cursor cursor = getContext().getContentResolver().query( + Cursor cursor = contentResolver.query( ContactsContract.CommonDataKinds.Email.CONTENT_URI, PROJECTION, ContactsContract.CommonDataKinds.Email.CONTACT_ID + "=?", new String[] { contactIdStr }, null); @@ -190,7 +243,7 @@ public class RecipientLoader extends AsyncTaskLoader> { Uri queryUriForNickname = ContactsContract.Data.CONTENT_URI; - return getContext().getContentResolver().query(queryUriForNickname, + return contentResolver.query(queryUriForNickname, PROJECTION_NICKNAME, ContactsContract.CommonDataKinds.Nickname.NAME + " LIKE ? AND " + Data.MIMETYPE + " = ?", @@ -215,7 +268,7 @@ public class RecipientLoader extends AsyncTaskLoader> { private void registerContentObserver() { if (observerContact != null) { observerContact = new ForceLoadContentObserver(); - getContext().getContentResolver().registerContentObserver(Email.CONTENT_URI, false, observerContact); + contentResolver.registerContentObserver(Email.CONTENT_URI, false, observerContact); } } @@ -225,8 +278,6 @@ public class RecipientLoader extends AsyncTaskLoader> { boolean hasContact = false; - final ContentResolver contentResolver = getContext().getContentResolver(); - Uri queryUri = Email.CONTENT_URI; Cursor nicknameCursor = getNicknameCursor(nickname); @@ -257,8 +308,6 @@ public class RecipientLoader extends AsyncTaskLoader> { private boolean fillContactDataFromNameAndEmail(String query, List recipients, Map recipientMap) { - - ContentResolver contentResolver = getContext().getContentResolver(); query = "%" + query + "%"; Uri queryUri = Email.CONTENT_URI; @@ -345,8 +394,7 @@ public class RecipientLoader extends AsyncTaskLoader> { Cursor cursor; Uri queryUri = Uri.parse("content://" + cryptoProvider + ".provider.exported/autocrypt_status"); try { - cursor = getContext().getContentResolver().query(queryUri, PROJECTION_CRYPTO_STATUS, null, - recipientAddresses, null); + cursor = contentResolver.query(queryUri, PROJECTION_CRYPTO_STATUS, null, recipientAddresses, null); } catch (SecurityException e) { // TODO escalate error to crypto status? return; @@ -390,7 +438,7 @@ public class RecipientLoader extends AsyncTaskLoader> { if (observerKey != null) { observerKey = new ForceLoadContentObserver(); - getContext().getContentResolver().registerContentObserver(queryUri, false, observerKey); + contentResolver.registerContentObserver(queryUri, false, observerKey); } } @@ -426,10 +474,10 @@ public class RecipientLoader extends AsyncTaskLoader> { super.onAbandon(); if (observerKey != null) { - getContext().getContentResolver().unregisterContentObserver(observerKey); + contentResolver.unregisterContentObserver(observerKey); } if (observerContact != null) { - getContext().getContentResolver().unregisterContentObserver(observerContact); + contentResolver.unregisterContentObserver(observerContact); } } } diff --git a/k9mail/src/test/java/com/fsck/k9/activity/compose/RecipientLoaderTest.java b/k9mail/src/test/java/com/fsck/k9/activity/compose/RecipientLoaderTest.java new file mode 100644 index 000000000..86c61c566 --- /dev/null +++ b/k9mail/src/test/java/com/fsck/k9/activity/compose/RecipientLoaderTest.java @@ -0,0 +1,144 @@ +package com.fsck.k9.activity.compose; + + +import java.util.List; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.MatrixCursor; +import android.net.Uri; + +import com.fsck.k9.K9RobolectricTestRunner; +import com.fsck.k9.mail.Address; +import com.fsck.k9.view.RecipientSelectView.Recipient; +import com.fsck.k9.view.RecipientSelectView.RecipientCryptoStatus; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +@SuppressWarnings("WeakerAccess") +@RunWith(K9RobolectricTestRunner.class) +public class RecipientLoaderTest { + static final String CRYPTO_PROVIDER = "cryptoProvider"; + static final String[] PROJECTION_CRYPTO_ADDRESSES = { "address", "uid_address" }; + static final String[] PROJECTION_CRYPTO_STATUS = { "address", "uid_key_status", "autocrypt_key_status" }; + static final Address CONTACT_ADDRESS_1 = Address.parse("Contact Name ")[0]; + static final Address CONTACT_ADDRESS_2 = Address.parse("Other Contact Name ")[0]; + static final String QUERYSTRING = "querystring"; + + + Context context; + ContentResolver contentResolver; + + + @Before + public void setUp() throws Exception { + context = mock(Context.class); + contentResolver = mock(ContentResolver.class); + + when(context.getContentResolver()).thenReturn(contentResolver); + } + + @Test + public void queryCryptoProvider() throws Exception { + RecipientLoader recipientLoader = new RecipientLoader(context, CRYPTO_PROVIDER, QUERYSTRING); + + setupQueryCryptoProvider("%" + QUERYSTRING + "%", CONTACT_ADDRESS_1, CONTACT_ADDRESS_2); + + List recipients = recipientLoader.loadInBackground(); + + assertEquals(2, recipients.size()); + assertEquals(CONTACT_ADDRESS_1, recipients.get(0).address); + assertEquals(CONTACT_ADDRESS_2, recipients.get(1).address); + assertEquals(RecipientCryptoStatus.UNAVAILABLE, recipients.get(0).getCryptoStatus()); + } + + @Test + public void queryCryptoStatus_unavailable() throws Exception { + RecipientLoader recipientLoader = new RecipientLoader(context, CRYPTO_PROVIDER, CONTACT_ADDRESS_1); + + setupCryptoProviderStatus(CONTACT_ADDRESS_1, "0", "0"); + + List recipients = recipientLoader.loadInBackground(); + + assertEquals(1, recipients.size()); + Recipient recipient = recipients.get(0); + assertEquals(CONTACT_ADDRESS_1, recipient.address); + assertEquals(RecipientCryptoStatus.UNAVAILABLE, recipient.getCryptoStatus()); + } + + @Test + public void queryCryptoStatus_autocrypt_untrusted() throws Exception { + RecipientLoader recipientLoader = new RecipientLoader(context, CRYPTO_PROVIDER, CONTACT_ADDRESS_1); + + setupCryptoProviderStatus(CONTACT_ADDRESS_1, "0", "1"); + + List recipients = recipientLoader.loadInBackground(); + + assertEquals(1, recipients.size()); + Recipient recipient = recipients.get(0); + assertEquals(CONTACT_ADDRESS_1, recipient.address); + assertEquals(RecipientCryptoStatus.AVAILABLE_UNTRUSTED, recipient.getCryptoStatus()); + } + + @Test + public void queryCryptoStatus_autocrypt_trusted() throws Exception { + RecipientLoader recipientLoader = new RecipientLoader(context, CRYPTO_PROVIDER, CONTACT_ADDRESS_1); + + setupCryptoProviderStatus(CONTACT_ADDRESS_1, "0", "2"); + + List recipients = recipientLoader.loadInBackground(); + + assertEquals(1, recipients.size()); + Recipient recipient = recipients.get(0); + assertEquals(CONTACT_ADDRESS_1, recipient.address); + assertEquals(RecipientCryptoStatus.AVAILABLE_TRUSTED, recipient.getCryptoStatus()); + } + + @Test + public void queryCryptoStatus_withHigherUidStatus() throws Exception { + RecipientLoader recipientLoader = new RecipientLoader(context, CRYPTO_PROVIDER, CONTACT_ADDRESS_1); + + setupCryptoProviderStatus(CONTACT_ADDRESS_1, "2", "1"); + + List recipients = recipientLoader.loadInBackground(); + + assertEquals(1, recipients.size()); + Recipient recipient = recipients.get(0); + assertEquals(CONTACT_ADDRESS_1, recipient.address); + assertEquals(RecipientCryptoStatus.AVAILABLE_TRUSTED, recipient.getCryptoStatus()); + } + + private void setupQueryCryptoProvider(String queriedAddress, Address... contactAddresses) { + MatrixCursor cursor = new MatrixCursor(PROJECTION_CRYPTO_ADDRESSES); + for (Address contactAddress : contactAddresses) { + cursor.addRow(new String[] { queriedAddress, contactAddress.toString() }); + } + + when(contentResolver + .query(eq(Uri.parse("content://" + CRYPTO_PROVIDER + ".provider.exported/autocrypt_status")), + aryEq(PROJECTION_CRYPTO_ADDRESSES), any(String.class), + aryEq(new String[] { queriedAddress }), + any(String.class))).thenReturn(cursor); + } + + private void setupCryptoProviderStatus(Address address, String uidStatus, String autocryptStatus) { + MatrixCursor cursorCryptoStatus = new MatrixCursor(PROJECTION_CRYPTO_STATUS); + cursorCryptoStatus.addRow(new String[] { address.getAddress(), uidStatus, autocryptStatus }); + + when(contentResolver + .query(eq(Uri.parse("content://" + CRYPTO_PROVIDER + ".provider.exported/autocrypt_status")), + aryEq(PROJECTION_CRYPTO_STATUS), any(String.class), + aryEq(new String[] { address.getAddress() }), + any(String.class))).thenReturn(cursorCryptoStatus); + } + +} \ No newline at end of file