Get contact info in real-time

This commit is contained in:
M66B 2019-01-19 13:21:21 +00:00
parent 274deddc88
commit c3de24c60f
6 changed files with 153 additions and 189 deletions

View File

@ -73,7 +73,6 @@ import org.xml.sax.XMLReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.Collator;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@ -414,9 +413,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
if (!outgoing && (avatars || identicons)) {
Bundle aargs = new Bundle();
aargs.putLong("id", message.id);
aargs.putString("uri", message.avatar);
if (message.from != null && message.from.length > 0)
aargs.putString("from", message.from[0].toString());
aargs.putSerializable("addresses", message.from);
new SimpleTask<ContactInfo>() {
@Override
@ -428,39 +425,16 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
@Override
protected ContactInfo onExecute(Context context, Bundle args) {
String uri = args.getString("uri");
Address[] addresses = (Address[]) args.getSerializable("addresses");
ContactInfo info = new ContactInfo();
ContentResolver resolver = context.getContentResolver();
ContactInfo info = ContactInfo.get(context, addresses);
if (contacts && avatars && uri != null)
try {
InputStream is = ContactsContract.Contacts.openContactPhotoInputStream(resolver, Uri.parse(uri));
if (is != null)
info.avatar = Drawable.createFromStream(is, "avatar");
} catch (SecurityException ex) {
Log.e(ex);
}
String from = args.getString("from");
if (info.avatar == null && identicons && from != null)
info.avatar = new BitmapDrawable(
if ((info == null || !info.hasPhoto()) && identicons) {
Drawable ident = new BitmapDrawable(
context.getResources(),
Identicon.generate(from, dp24, 5, "light".equals(theme)));
if (contacts && uri != null) {
Cursor cursor = null;
try {
cursor = resolver.query(
Uri.parse(uri),
new String[]{ContactsContract.Contacts.DISPLAY_NAME},
null, null, null);
if (cursor != null && cursor.moveToNext())
info.displayName = cursor.getString(0);
} finally {
if (cursor != null)
cursor.close();
}
Identicon.generate(addresses[0].toString(),
dp24, 5, "light".equals(theme)));
info = new ContactInfo(ident, (info == null ? null : info.getDisplayName()));
}
return info;
@ -471,19 +445,16 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
long id = args.getLong("id");
if ((long) ivAvatar.getTag() == id) {
if (info.avatar == null)
if (info == null || !info.hasPhoto())
ivAvatar.setImageResource(R.drawable.baseline_person_24);
else
ivAvatar.setImageDrawable(info.avatar);
ivAvatar.setImageDrawable(info.getPhotoDrawable());
ivAvatar.setVisibility(View.VISIBLE);
} else
Log.i("Skipping avatar");
}
if ((long) tvFrom.getTag() == id) {
if (info.displayName != null) {
Log.i("Using contact name=" + info.displayName);
tvFrom.setText(info.displayName);
}
if (info != null && info.hasDisplayName())
tvFrom.setText(info.getDisplayName());
}
}
@ -2047,7 +2018,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
this.threading = prefs.getBoolean("threading", true);
this.contacts = (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED);
this.avatars = (prefs.getBoolean("avatars", true) && this.contacts);
this.avatars = prefs.getBoolean("avatars", true);
this.identicons = prefs.getBoolean("identicons", false);
this.preview = prefs.getBoolean("preview", false);
this.confirm = prefs.getBoolean("confirm", false);
@ -2203,11 +2174,6 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
return key;
}
private class ContactInfo {
Drawable avatar;
String displayName;
}
interface IProperties {
void setValue(String name, long id, boolean enabled);

View File

@ -0,0 +1,123 @@
package eu.faircode.email;
import android.Manifest;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.ContactsContract;
import java.io.InputStream;
import javax.mail.Address;
import javax.mail.internet.InternetAddress;
import androidx.core.content.ContextCompat;
public class ContactInfo {
private InputStream is;
private Drawable photo;
private String displayName;
private Uri lookupUri;
ContactInfo() {
}
ContactInfo(String displayName) {
this.displayName = displayName;
}
ContactInfo(Drawable photo, String displayName) {
this.photo = photo;
this.displayName = displayName;
}
Bitmap getPhotoBitmap() {
return BitmapFactory.decodeStream(is);
}
Drawable getPhotoDrawable() {
if (photo != null)
return photo;
if (is == null)
return null;
return Drawable.createFromStream(is, displayName == null ? "Photo" : displayName);
}
boolean hasPhoto() {
return (is != null || photo != null);
}
String getDisplayName() {
return displayName;
}
boolean hasDisplayName() {
return (displayName != null);
}
Uri getLookupUri() {
return lookupUri;
}
boolean hasLookupUri() {
return (lookupUri != null);
}
static ContactInfo get(Context context, Address[] addresses) {
if (addresses == null)
return null;
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED)
return null;
try {
for (Address address : addresses) {
Cursor cursor = null;
try {
ContentResolver resolver = context.getContentResolver();
cursor = resolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI,
new String[]{
ContactsContract.CommonDataKinds.Photo.CONTACT_ID,
ContactsContract.Contacts.LOOKUP_KEY,
ContactsContract.Contacts.DISPLAY_NAME
},
ContactsContract.CommonDataKinds.Email.ADDRESS + " = ?",
new String[]{
((InternetAddress) address).getAddress()
}, null);
if (cursor != null && cursor.moveToNext()) {
int colContactId = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Photo.CONTACT_ID);
int colLookupKey = cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY);
int colDisplayName = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
long contactId = cursor.getLong(colContactId);
String lookupKey = cursor.getString(colLookupKey);
Uri lookupUri = ContactsContract.Contacts.getLookupUri(contactId, lookupKey);
ContactInfo info = new ContactInfo();
info.is = ContactsContract.Contacts.openContactPhotoInputStream(resolver, lookupUri);
info.displayName = cursor.getString(colDisplayName);
info.lookupUri = lookupUri;
return info;
}
} finally {
if (cursor != null)
cursor.close();
}
}
} catch (Throwable ex) {
Log.e(ex);
}
return null;
}
}

View File

@ -19,16 +19,11 @@ package eu.faircode.email;
Copyright 2018-2019 by Marcel Bokhorst (M66B)
*/
import android.Manifest;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import java.io.BufferedReader;
import java.io.BufferedWriter;
@ -43,10 +38,8 @@ import java.util.Map;
import java.util.Random;
import javax.mail.Address;
import javax.mail.internet.InternetAddress;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index;
@ -105,7 +98,7 @@ public class EntityMessage implements Serializable {
public String deliveredto;
public String inreplyto;
public String thread; // compose = null
public String avatar; // Contact lookup URI
public String avatar; // obsolete
public String sender; // sort key
public Address[] from;
public Address[] to;
@ -210,60 +203,6 @@ public class EntityMessage implements Serializable {
return new File(dir, Long.toString(id));
}
static String getLookupUri(Context context, Address[] froms) {
if (froms == null)
return null;
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED)
return null;
try {
for (Address from : froms) {
String email = ((InternetAddress) from).getAddress();
synchronized (emailLookupUri) {
Uri lookupUri = emailLookupUri.get(email);
if (lookupUri != null)
return lookupUri.toString();
}
Cursor cursor = null;
try {
ContentResolver resolver = context.getContentResolver();
cursor = resolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI,
new String[]{
ContactsContract.CommonDataKinds.Photo.CONTACT_ID,
ContactsContract.Contacts.LOOKUP_KEY
},
ContactsContract.CommonDataKinds.Email.ADDRESS + " = ?",
new String[]{email}, null);
if (cursor != null && cursor.moveToNext()) {
int colContactId = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Photo.CONTACT_ID);
int colLookupKey = cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY);
long contactId = cursor.getLong(colContactId);
String lookupKey = cursor.getString(colLookupKey);
Uri lookupUri = ContactsContract.Contacts.getLookupUri(contactId, lookupKey);
synchronized (emailLookupUri) {
emailLookupUri.put(email, lookupUri);
}
return lookupUri.toString();
}
} finally {
if (cursor != null)
cursor.close();
}
}
} catch (Throwable ex) {
Log.e(ex);
}
return null;
}
static void snooze(Context context, long id, Long wakeup) {
Intent snoozed = new Intent(context, ServiceSynchronize.class);
snoozed.setAction("snooze:" + id);
@ -294,7 +233,6 @@ public class EntityMessage implements Serializable {
//(this.deliveredto == null ? other.deliveredto == null : this.deliveredto.equals(other.deliveredto)) &&
//(this.inreplyto == null ? other.inreplyto == null : this.inreplyto.equals(other.inreplyto)) &&
(this.thread == null ? other.thread == null : this.thread.equals(other.thread)) &&
(this.avatar == null ? other.avatar == null : this.avatar.equals(other.avatar)) &&
MessageHelper.equal(this.from, other.from) &&
MessageHelper.equal(this.to, other.to) &&
MessageHelper.equal(this.cc, other.cc) &&
@ -342,7 +280,6 @@ public class EntityMessage implements Serializable {
(this.deliveredto == null ? other.deliveredto == null : this.deliveredto.equals(other.deliveredto)) &&
(this.inreplyto == null ? other.inreplyto == null : this.inreplyto.equals(other.inreplyto)) &&
(this.thread == null ? other.thread == null : this.thread.equals(other.thread)) &&
(this.avatar == null ? other.avatar == null : this.avatar.equals(other.avatar)) &&
MessageHelper.equal(this.from, other.from) &&
MessageHelper.equal(this.to, other.to) &&
MessageHelper.equal(this.cc, other.cc) &&

View File

@ -214,7 +214,6 @@ public class EntityRule {
reply.subject = context.getString(R.string.title_subject_reply, message.subject == null ? "" : message.subject);
reply.sender = MessageHelper.getSortKey(reply.from);
reply.received = new Date().getTime();
reply.avatar = EntityMessage.getLookupUri(context, reply.from);
reply.id = db.message().insertMessage(reply);
reply.write(context, body);
db.message().setMessageContent(reply.id, true, HtmlHelper.getPreview(body));

View File

@ -1551,7 +1551,6 @@ public class FragmentCompose extends FragmentBase {
result.draft.sender = MessageHelper.getSortKey(result.draft.from);
result.draft.received = new Date().getTime();
result.draft.avatar = EntityMessage.getLookupUri(context, result.draft.from);
result.draft.id = db.message().insertMessage(result.draft);
result.draft.write(context, body == null ? "" : body);

View File

@ -19,7 +19,6 @@ package eu.faircode.email;
Copyright 2018-2019 by Marcel Bokhorst (M66B)
*/
import android.Manifest;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
@ -31,10 +30,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.Icon;
import android.media.RingtoneManager;
import android.net.ConnectivityManager;
import android.net.Network;
@ -47,7 +43,6 @@ import android.os.Handler;
import android.os.PowerManager;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.text.Html;
import android.text.TextUtils;
import android.util.LongSparseArray;
@ -70,7 +65,6 @@ import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketException;
import java.net.SocketTimeoutException;
@ -483,10 +477,6 @@ public class ServiceSynchronize extends LifecycleService {
return notifications;
boolean pro = Helper.isPro(this);
boolean contacts =
(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
// https://developer.android.com/training/notify-user/group
@ -495,28 +485,13 @@ public class ServiceSynchronize extends LifecycleService {
String summary = getResources().getQuantityString(
R.plurals.title_notification_unseen, messages.size(), messages.size());
Map<TupleMessageEx, String> messageFrom = new HashMap<>();
// Get contact info
Map<TupleMessageEx, ContactInfo> messageContact = new HashMap<>();
for (TupleMessageEx message : messages) {
String from = null;
if (!TextUtils.isEmpty(message.avatar) && contacts) {
Cursor cursor = null;
try {
cursor = getContentResolver().query(
Uri.parse(message.avatar),
new String[]{ContactsContract.Contacts.DISPLAY_NAME},
null, null, null);
if (cursor != null && cursor.moveToNext())
from = cursor.getString(0);
} finally {
if (cursor != null)
cursor.close();
}
}
if (from == null)
from = MessageHelper.formatAddressesShort(message.from);
messageFrom.put(message, from);
ContactInfo info = ContactInfo.get(this, message.from);
if (info == null)
info = new ContactInfo(MessageHelper.formatAddressesShort(message.from));
messageContact.put(message, info);
}
// Build pending intent
@ -596,7 +571,7 @@ public class ServiceSynchronize extends LifecycleService {
DateFormat df = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT);
StringBuilder sb = new StringBuilder();
for (EntityMessage message : messages) {
sb.append("<strong>").append(messageFrom.get(message)).append("</strong>");
sb.append("<strong>").append(messageContact.get(message).getDisplayName()).append("</strong>");
if (!TextUtils.isEmpty(message.subject))
sb.append(": ").append(message.subject);
sb.append(" ").append(df.format(message.received));
@ -611,6 +586,8 @@ public class ServiceSynchronize extends LifecycleService {
notifications.add(builder.build());
for (TupleMessageEx message : messages) {
ContactInfo info = messageContact.get(message);
Bundle args = new Bundle();
args.putLong("id", message.content ? message.id : -message.id);
@ -667,7 +644,7 @@ public class ServiceSynchronize extends LifecycleService {
mbuilder
.addExtras(args)
.setSmallIcon(R.drawable.baseline_email_white_24)
.setContentTitle(messageFrom.get(message))
.setContentTitle(info.getDisplayName())
.setSubText(message.accountName + " · " + folderName)
.setContentIntent(piContent)
.setWhen(message.received)
@ -702,47 +679,16 @@ public class ServiceSynchronize extends LifecycleService {
mbuilder.setStyle(new Notification.BigTextStyle().bigText(ex.toString()));
}
if (!TextUtils.isEmpty(message.avatar)) {
if (contacts) {
Cursor cursor = null;
try {
cursor = getContentResolver().query(
Uri.parse(message.avatar),
new String[]{
ContactsContract.Contacts._ID,
ContactsContract.Contacts.LOOKUP_KEY
},
null, null, null);
if (cursor != null && cursor.moveToNext()) {
if (true || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Uri uri = ContactsContract.Contacts.getLookupUri(
cursor.getLong(cursor.getColumnIndex(ContactsContract.Contacts._ID)),
cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)));
InputStream is = ContactsContract.Contacts.openContactPhotoInputStream(
getContentResolver(), uri);
mbuilder.setLargeIcon(BitmapFactory.decodeStream(is));
} else {
Uri photo = Uri.withAppendedPath(
ContactsContract.Contacts.CONTENT_URI,
cursor.getLong(0) + "/photo");
mbuilder.setLargeIcon(Icon.createWithContentUri(photo));
}
}
} catch (Throwable ex) {
Log.e(ex);
} finally {
if (cursor != null)
cursor.close();
}
}
if (info.hasPhoto())
mbuilder.setLargeIcon(info.getPhotoBitmap());
if (info.hasLookupUri())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
mbuilder.addPerson(new Person.Builder()
.setUri(message.avatar)
.setUri(info.getLookupUri().toString())
.build());
else
mbuilder.addPerson(message.avatar);
}
mbuilder.addPerson(info.getLookupUri().toString());
if (message.accountColor != null) {
mbuilder.setColor(message.accountColor);
@ -2638,12 +2584,6 @@ public class ServiceSynchronize extends LifecycleService {
if (message == null)
return;
if (message.avatar == null && !folder.isOutgoing()) {
message.avatar = EntityMessage.getLookupUri(context, message.from);
if (message.avatar != null)
db.message().updateMessage(message);
}
if (download) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
long maxSize = prefs.getInt("download", 32768);