Merge pull request #3244 from k9mail/mynimu-directShare_GH2550
Add Direct Share support
This commit is contained in:
commit
119144d256
5 changed files with 263 additions and 11 deletions
|
@ -250,6 +250,10 @@
|
|||
<action android:name="org.autocrypt.PEER_ACTION"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.chooser.chooser_target_service"
|
||||
android:value="com.fsck.k9.service.K9ChooserTargetService" />
|
||||
</activity>
|
||||
|
||||
<!-- Search Activity - searchable -->
|
||||
|
@ -416,6 +420,15 @@
|
|||
android:name=".service.DatabaseUpgradeService"
|
||||
android:exported="false"/>
|
||||
|
||||
|
||||
<service
|
||||
android:name="com.fsck.k9.service.K9ChooserTargetService"
|
||||
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE" >
|
||||
<intent-filter>
|
||||
<action android:name="android.service.chooser.ChooserTargetService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name=".provider.AttachmentProvider"
|
||||
android:authorities="${applicationId}.attachmentprovider"
|
||||
|
|
|
@ -17,6 +17,7 @@ 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.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.fsck.k9.R;
|
||||
|
@ -112,6 +113,16 @@ public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
|
|||
private List<Recipient> cachedRecipients;
|
||||
private ForceLoadContentObserver observerContact, observerKey;
|
||||
|
||||
private RecipientLoader(Context context) {
|
||||
super(context);
|
||||
this.query = null;
|
||||
this.lookupKeyUri = null;
|
||||
this.addresses = null;
|
||||
this.contactUri = null;
|
||||
|
||||
this.cryptoProvider = null;
|
||||
this.contentResolver = context.getContentResolver();
|
||||
}
|
||||
|
||||
public RecipientLoader(Context context, String cryptoProvider, String query) {
|
||||
super(context);
|
||||
|
@ -146,6 +157,15 @@ public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
|
|||
contentResolver = context.getContentResolver();
|
||||
}
|
||||
|
||||
public static RecipientLoader getMostContactedRecipientLoader(Context context, final int maxRecipients) {
|
||||
return new RecipientLoader(context) {
|
||||
@Override
|
||||
public List<Recipient> loadInBackground() {
|
||||
return super.fillContactDataBySortOrder(maxRecipients);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Recipient> loadInBackground() {
|
||||
List<Recipient> recipients = new ArrayList<>();
|
||||
|
@ -167,6 +187,11 @@ public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
|
|||
throw new IllegalStateException("loader must be initialized with query or list of addresses!");
|
||||
}
|
||||
|
||||
return fillCryptoStatusData(recipients, recipientMap);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<Recipient> fillCryptoStatusData(List<Recipient> recipients, Map<String, Recipient> recipientMap) {
|
||||
if (recipients.isEmpty()) {
|
||||
return recipients;
|
||||
}
|
||||
|
@ -178,6 +203,7 @@ public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
|
|||
return recipients;
|
||||
}
|
||||
|
||||
|
||||
private void fillContactDataFromCryptoProvider(String query, List<Recipient> recipients,
|
||||
Map<String, Recipient> recipientMap) {
|
||||
Cursor cursor;
|
||||
|
@ -279,7 +305,7 @@ public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
|
|||
|
||||
boolean foundValidCursor = false;
|
||||
foundValidCursor |= fillContactDataFromNickname(query, recipients, recipientMap);
|
||||
foundValidCursor |= fillContactDataFromNameAndEmail(query, recipients, recipientMap);
|
||||
foundValidCursor |= fillContactDataFromNameAndEmail(query, recipients, recipientMap, null);
|
||||
|
||||
if (foundValidCursor) {
|
||||
Collections.sort(recipients, RECIPIENT_COMPARATOR);
|
||||
|
@ -317,7 +343,7 @@ public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
|
|||
.query(queryUri, PROJECTION, selection, new String[] { id }, SORT_ORDER);
|
||||
|
||||
String contactNickname = nicknameCursor.getString(INDEX_NICKNAME);
|
||||
fillContactDataFromCursor(cursor, recipients, recipientMap, contactNickname);
|
||||
fillContactDataFromCursor(cursor, recipients, recipientMap, contactNickname, null);
|
||||
|
||||
hasContact = true;
|
||||
}
|
||||
|
@ -328,9 +354,25 @@ public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
|
|||
return hasContact;
|
||||
}
|
||||
|
||||
private List<Recipient> fillContactDataBySortOrder(int maxRecipients) {
|
||||
List<Recipient> recipients = new ArrayList<>();
|
||||
|
||||
Uri queryUri = Email.CONTENT_URI;
|
||||
Cursor cursor = contentResolver.query(queryUri, PROJECTION, null, null, SORT_ORDER);
|
||||
|
||||
if (cursor == null) {
|
||||
return recipients;
|
||||
}
|
||||
|
||||
fillContactDataFromCursor(cursor, recipients, new HashMap<String, Recipient>(), null, maxRecipients);
|
||||
|
||||
return recipients;
|
||||
}
|
||||
|
||||
|
||||
private boolean fillContactDataFromNameAndEmail(String query, List<Recipient> recipients,
|
||||
Map<String, Recipient> recipientMap) {
|
||||
Map<String, Recipient> recipientMap, Integer maxTargets) {
|
||||
|
||||
query = "%" + query + "%";
|
||||
|
||||
Uri queryUri = Email.CONTENT_URI;
|
||||
|
@ -352,13 +394,13 @@ public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
|
|||
|
||||
private void fillContactDataFromCursor(Cursor cursor, List<Recipient> recipients,
|
||||
Map<String, Recipient> recipientMap) {
|
||||
fillContactDataFromCursor(cursor, recipients, recipientMap, null);
|
||||
fillContactDataFromCursor(cursor, recipients, recipientMap, null, null);
|
||||
}
|
||||
|
||||
private void fillContactDataFromCursor(Cursor cursor, List<Recipient> recipients,
|
||||
Map<String, Recipient> recipientMap, @Nullable String prefilledName) {
|
||||
Map<String, Recipient> recipientMap, @Nullable String prefilledName, @Nullable Integer maxRecipients) {
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
while (cursor.moveToNext() && (maxRecipients == null || recipients.size() < maxRecipients)) {
|
||||
String name = prefilledName != null ? prefilledName : cursor.getString(INDEX_NAME);
|
||||
|
||||
String email = cursor.getString(INDEX_EMAIL);
|
||||
|
@ -508,4 +550,5 @@ public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
|
|||
contentResolver.unregisterContentObserver(observerContact);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,12 +9,15 @@ import java.util.regex.Pattern;
|
|||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.CompressFormat;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.support.annotation.WorkerThread;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.ImageView;
|
||||
|
||||
|
@ -32,6 +35,7 @@ import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder;
|
|||
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
|
||||
import com.bumptech.glide.load.resource.file.FileToStreamDecoder;
|
||||
import com.bumptech.glide.load.resource.transcode.BitmapToGlideDrawableTranscoder;
|
||||
import com.bumptech.glide.request.FutureTarget;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.fsck.k9.helper.Contacts;
|
||||
|
@ -56,7 +60,7 @@ public class ContactPictureLoader {
|
|||
private static final String FALLBACK_CONTACT_LETTER = "?";
|
||||
|
||||
|
||||
private Resources mResources;
|
||||
private final Context context;
|
||||
private Contacts mContactsHelper;
|
||||
private int mPictureSizeInPx;
|
||||
|
||||
|
@ -102,11 +106,11 @@ public class ContactPictureLoader {
|
|||
* use a dynamically calculated background color.
|
||||
*/
|
||||
public ContactPictureLoader(Context context, int defaultBackgroundColor) {
|
||||
Context appContext = context.getApplicationContext();
|
||||
mResources = appContext.getResources();
|
||||
mContactsHelper = Contacts.getInstance(appContext);
|
||||
this.context = context.getApplicationContext();
|
||||
mContactsHelper = Contacts.getInstance(this.context);
|
||||
|
||||
float scale = mResources.getDisplayMetrics().density;
|
||||
Resources resources = context.getResources();
|
||||
float scale = resources.getDisplayMetrics().density;
|
||||
mPictureSizeInPx = (int) (PICTURE_SIZE * scale);
|
||||
|
||||
mDefaultBackgroundColor = defaultBackgroundColor;
|
||||
|
@ -172,6 +176,37 @@ public class ContactPictureLoader {
|
|||
}
|
||||
}
|
||||
|
||||
public Bitmap loadContactPictureIcon(Recipient recipient) {
|
||||
return loadContactPicture(recipient.photoThumbnailUri, recipient.address);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private Bitmap loadContactPicture(Uri photoUri, Address address) {
|
||||
FutureTarget<Bitmap> bitmapTarget;
|
||||
if (photoUri != null) {
|
||||
bitmapTarget = Glide.with(context)
|
||||
.load(photoUri)
|
||||
.asBitmap()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.dontAnimate()
|
||||
.into(mPictureSizeInPx, mPictureSizeInPx);
|
||||
} else {
|
||||
bitmapTarget = Glide.with(context)
|
||||
.using(new FallbackGlideModelLoader(), FallbackGlideParams.class)
|
||||
.from(FallbackGlideParams.class)
|
||||
.as(Bitmap.class)
|
||||
.decoder(new FallbackGlideBitmapDecoder(context))
|
||||
.encoder(new BitmapEncoder(CompressFormat.PNG, 0))
|
||||
.cacheDecoder(new FileToStreamDecoder<>(new StreamBitmapDecoder(context)))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.load(new FallbackGlideParams(address))
|
||||
.dontAnimate()
|
||||
.into(mPictureSizeInPx, mPictureSizeInPx);
|
||||
}
|
||||
|
||||
return loadIgnoringErors(bitmapTarget);
|
||||
}
|
||||
|
||||
private int calcUnknownContactColor(Address address) {
|
||||
if (mDefaultBackgroundColor != 0) {
|
||||
return mDefaultBackgroundColor;
|
||||
|
@ -271,4 +306,14 @@ public class ContactPictureLoader {
|
|||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Nullable
|
||||
private <T> T loadIgnoringErors(FutureTarget<T> target) {
|
||||
try {
|
||||
return target.get();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
package com.fsck.k9.service;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.service.chooser.ChooserTarget;
|
||||
import android.service.chooser.ChooserTargetService;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.fsck.k9.activity.MessageCompose;
|
||||
import com.fsck.k9.activity.compose.RecipientLoader;
|
||||
import com.fsck.k9.activity.misc.ContactPictureLoader;
|
||||
import com.fsck.k9.helper.ContactPicture;
|
||||
import com.fsck.k9.mail.Address;
|
||||
import com.fsck.k9.view.RecipientSelectView.Recipient;
|
||||
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public class K9ChooserTargetService extends ChooserTargetService {
|
||||
private static final int MAX_TARGETS = 5;
|
||||
|
||||
private RecipientLoader recipientLoader;
|
||||
private ContactPictureLoader contactPictureLoader;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Context applicationContext = getApplicationContext();
|
||||
recipientLoader = RecipientLoader.getMostContactedRecipientLoader(applicationContext, MAX_TARGETS);
|
||||
contactPictureLoader = ContactPicture.getContactPictureLoader(applicationContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName, IntentFilter matchedFilter) {
|
||||
List<Recipient> recipients = recipientLoader.loadInBackground();
|
||||
|
||||
return createChooserTargets(recipients);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<ChooserTarget> createChooserTargets(List<Recipient> recipients) {
|
||||
float score = 1.0f;
|
||||
|
||||
List<ChooserTarget> targets = new ArrayList<>();
|
||||
ComponentName componentName = new ComponentName(this, MessageCompose.class);
|
||||
for (Recipient recipient : recipients) {
|
||||
Bundle intentExtras = prepareIntentExtras(recipient);
|
||||
Icon icon = loadRecipientIcon(recipient);
|
||||
|
||||
ChooserTarget chooserTarget =
|
||||
new ChooserTarget(recipient.getDisplayNameOrAddress(), icon, score, componentName, intentExtras);
|
||||
targets.add(chooserTarget);
|
||||
|
||||
score -= 0.1;
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Bundle prepareIntentExtras(Recipient recipient) {
|
||||
Address address = recipient.address;
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putStringArray(Intent.EXTRA_EMAIL, new String[] { address.toString() });
|
||||
extras.putStringArray(Intent.EXTRA_CC, new String[0]);
|
||||
extras.putStringArray(Intent.EXTRA_BCC, new String[0]);
|
||||
|
||||
return extras;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Icon loadRecipientIcon(Recipient recipient) {
|
||||
Bitmap bitmap = contactPictureLoader.loadContactPictureIcon(recipient);
|
||||
if (bitmap == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Icon.createWithBitmap(bitmap);
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ import static org.junit.Assert.assertEquals;
|
|||
import static org.mockito.AdditionalMatchers.aryEq;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Matchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
@ -55,6 +56,8 @@ public class RecipientLoaderTest {
|
|||
static final String TYPE = "" + TYPE_HOME;
|
||||
static final String[] CONTACT_1 =
|
||||
new String[] { "0", "Bob", "bob", "bob@host.com", TYPE, null, "1", null, "100", "Bob" };
|
||||
static final String[] CONTACT_2 =
|
||||
new String[] { "2", "Bob2", "bob2", "bob2@host.com", TYPE, null, "2", null, "99", "Bob2" };
|
||||
static final String[] CONTACT_NO_EMAIL =
|
||||
new String[] { "0", "Bob", "bob", null, TYPE, null, "1", null, "10", "Bob_noMail" };
|
||||
static final String[] CONTACT_WITH_NICKNAME_NOT_CONTACTED =
|
||||
|
@ -247,4 +250,59 @@ public class RecipientLoaderTest {
|
|||
assertEquals("bob@host.com", recipients.get(0).address.getAddress());
|
||||
assertEquals("eve_notContacted@host.com", recipients.get(1).address.getAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMostContactedFoundMore() throws Exception {
|
||||
int maxTargets = 1;
|
||||
setupContactProvider(CONTACT_1, CONTACT_2);
|
||||
|
||||
RecipientLoader recipientLoader = RecipientLoader.getMostContactedRecipientLoader(context, maxTargets);
|
||||
List<Recipient> recipients = recipientLoader.loadInBackground();
|
||||
|
||||
assertEquals(maxTargets, recipients.size());
|
||||
assertEquals("bob@host.com", recipients.get(0).address.getAddress());
|
||||
assertEquals(RecipientCryptoStatus.UNDEFINED, recipients.get(0).getCryptoStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMostContactedFoundLess() throws Exception {
|
||||
int maxTargets = 5;
|
||||
setupContactProvider(CONTACT_1, CONTACT_2);
|
||||
|
||||
RecipientLoader recipientLoader = RecipientLoader.getMostContactedRecipientLoader(context, maxTargets);
|
||||
List<Recipient> recipients = recipientLoader.loadInBackground();
|
||||
|
||||
assertEquals(2, recipients.size());
|
||||
assertEquals("bob@host.com", recipients.get(0).address.getAddress());
|
||||
assertEquals(RecipientCryptoStatus.UNDEFINED, recipients.get(0).getCryptoStatus());
|
||||
assertEquals("bob2@host.com", recipients.get(1).address.getAddress());
|
||||
assertEquals(RecipientCryptoStatus.UNDEFINED, recipients.get(1).getCryptoStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMostContactedFoundNothing() throws Exception {
|
||||
int maxTargets = 5;
|
||||
setupContactProvider();
|
||||
|
||||
|
||||
RecipientLoader recipientLoader = RecipientLoader.getMostContactedRecipientLoader(context, maxTargets);
|
||||
List<Recipient> recipients = recipientLoader.loadInBackground();
|
||||
|
||||
assertEquals(0, recipients.size());
|
||||
}
|
||||
|
||||
|
||||
private void setupContactProvider(String[]... contacts) {
|
||||
MatrixCursor cursor = new MatrixCursor(PROJECTION);
|
||||
for (String[] contact : contacts) {
|
||||
cursor.addRow(contact);
|
||||
}
|
||||
when(contentResolver
|
||||
.query(eq(Email.CONTENT_URI),
|
||||
aryEq(PROJECTION),
|
||||
isNull(String.class),
|
||||
isNull(String[].class),
|
||||
any(String.class))).thenReturn(cursor);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue