From dd672480669309a7da9b3433bdd0cd4f34087bea Mon Sep 17 00:00:00 2001 From: "daniel.boehrs" Date: Thu, 1 Nov 2018 16:21:43 +0100 Subject: [PATCH] avatars for contact / new chat view #55 --- .../securesms/CreateProfileActivity.java | 5 +- .../securesms/GroupCreateActivity.java | 29 +----- .../securesms/contacts/ContactAccessor.java | 20 ++-- .../contacts/ContactSelectionListItem.java | 8 ++ .../avatars/LocalFileContactPhoto.java | 2 +- .../avatars/MyProfileContactPhoto.java | 2 +- .../contacts/avatars/SystemContactPhoto.java | 6 +- .../jobs/RetrieveProfileAvatarJob.java | 4 +- .../securesms/profiles/AvatarHelper.java | 91 ++++++++++++++----- src/org/thoughtcrime/securesms/util/Hash.java | 22 +++++ .../securesms/util/TextSecurePreferences.java | 23 +++++ 11 files changed, 145 insertions(+), 67 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/util/Hash.java diff --git a/src/org/thoughtcrime/securesms/CreateProfileActivity.java b/src/org/thoughtcrime/securesms/CreateProfileActivity.java index 03ba47070..ab3dc8f3b 100644 --- a/src/org/thoughtcrime/securesms/CreateProfileActivity.java +++ b/src/org/thoughtcrime/securesms/CreateProfileActivity.java @@ -38,7 +38,6 @@ import com.soundcloud.android.crop.Crop; import org.thoughtcrime.securesms.components.InputAwareLayout; import org.thoughtcrime.securesms.components.emoji.EmojiDrawer; -import org.thoughtcrime.securesms.components.emoji.EmojiToggle; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; @@ -309,7 +308,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje private void initializeProfileAvatar(boolean excludeSystem) { String address = DcHelper.get(this, DcHelper.CONFIG_ADDRESS); - if (AvatarHelper.getAvatarFile(this, address).exists() && AvatarHelper.getAvatarFile(this, address).length() > 0) { + if (AvatarHelper.getSelfAvatarFile(this, address).exists() && AvatarHelper.getSelfAvatarFile(this, address).length() > 0) { new AsyncTask() { @Override protected byte[] doInBackground(Void... params) { @@ -452,7 +451,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje TextSecurePreferences.setProfileName(context, name); try { - AvatarHelper.setAvatar(CreateProfileActivity.this, DcHelper.get(context, DcHelper.CONFIG_ADDRESS), avatarBytes); + AvatarHelper.setSelfAvatar(CreateProfileActivity.this, DcHelper.get(context, DcHelper.CONFIG_ADDRESS), avatarBytes); TextSecurePreferences.setProfileAvatarId(CreateProfileActivity.this, new SecureRandom().nextInt()); } catch (IOException e) { Log.w(TAG, e); diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index a48d2f243..a250b92fc 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -63,8 +63,6 @@ import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.whispersystems.libsignal.util.guava.Optional; import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -298,7 +296,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity dcContext.addContactToChat(chatId, contactId); } } - setGroupAvatar(chatId); + AvatarHelper.setGroupAvatar(this, chatId, avatarBmp); Intent intent = new Intent(this, ConversationActivity.class); intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, chatId); @@ -306,29 +304,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity finish(); } - private void setGroupAvatar(int chatId) { - if(avatarBmp == null) { - return; - } - String avatarPath = dcContext.getChat(chatId).getProfileImage(); - if (avatarPath != null && !avatarPath.isEmpty()) { - File oldImage = new File(avatarPath); - if (oldImage.exists()) { - //noinspection ResultOfMethodCallIgnored - oldImage.delete(); - } - } - avatarPath = AvatarHelper.getFilePathForGroupAvatar(this, chatId, System.currentTimeMillis()); - FileOutputStream outStream; - try { - outStream = new FileOutputStream(avatarPath); - avatarBmp.compress(Bitmap.CompressFormat.JPEG, 85, outStream); - dcContext.setChatProfileImage(chatId, avatarPath); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - } - private boolean showGroupNameEmptyToast(String groupName) { if(groupName == null) { Toast.makeText(this, getString(R.string.GroupCreateActivity_please_enter_group_name), Toast.LENGTH_LONG).show(); @@ -348,7 +323,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity dcContext.setChatName(editGroupChatId, groupName); updateGroupParticipants(); - setGroupAvatar(editGroupChatId); + AvatarHelper.setGroupAvatar(this, editGroupChatId, avatarBmp); Intent intent = new Intent(); Recipient recipient = dcContext.getRecipient(ApplicationDcContext.RECIPIENT_TYPE_CHAT, editGroupChatId); diff --git a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java index 031bfa28c..eaf495305 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java @@ -33,6 +33,8 @@ import android.text.TextUtils; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.util.Hash; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import java.util.ArrayList; import java.util.Collection; @@ -61,7 +63,7 @@ public class ContactAccessor { private static final int CONTACT_CURSOR_MAIL = 1; - private static final int CONTACT_CURSOR_PHOTO = 2; + private static final int CONTACT_CURSOR_CONTACT_ID = 2; public static final String PUSH_COLUMN = "push"; @@ -86,7 +88,7 @@ public class ContactAccessor { } public Cursor getAllSystemContacts(Context context) { - String[] projection = {ContactsContract.Data.DISPLAY_NAME, ContactsContract.CommonDataKinds.Email.ADDRESS, Phone.PHOTO_ID}; + String[] projection = {ContactsContract.Data.DISPLAY_NAME, ContactsContract.CommonDataKinds.Email.ADDRESS, ContactsContract.Data.CONTACT_ID}; return context.getContentResolver().query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, projection, null, null, null); } @@ -94,15 +96,16 @@ public class ContactAccessor { Cursor systemContactsCursor = getAllSystemContacts(context); StringBuilder result = new StringBuilder(); List mailList = new ArrayList<>(); + Set contactPhotoIdentifiers = new HashSet<>(); while (systemContactsCursor != null && systemContactsCursor.moveToNext()) { String name = systemContactsCursor.getString(CONTACT_CURSOR_NAME); String mail = systemContactsCursor.getString(CONTACT_CURSOR_MAIL); - /* - TODO - - Persist this data to get it via DcContact.getProfileImage(); - - Copy non existing images to the Delta avatar directory - - Use: String contactPhotoUri = systemContactsCursor.getString(CONTACT_CURSOR_PHOTO); - */ + String contactId = systemContactsCursor.getString(CONTACT_CURSOR_CONTACT_ID); + if (contactId != null) { + String identifier = name + mail; + String hashedIdentifierAndId = Hash.sha256(identifier) + "|" + contactId; + contactPhotoIdentifiers.add(hashedIdentifierAndId); + } if (mail != null && !mail.isEmpty() && !mailList.contains(mail)) { mailList.add(mail); if (name.isEmpty()) { @@ -111,6 +114,7 @@ public class ContactAccessor { result.append(name).append("\n").append(mail).append("\n"); } } + TextSecurePreferences.setSystemContactPhotos(context, contactPhotoIdentifiers); return result.toString(); } diff --git a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java index 6b1235d84..331d17dad 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.contacts; import android.content.Context; +import android.net.Uri; import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.View; @@ -17,6 +18,8 @@ import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; +import org.thoughtcrime.securesms.util.Hash; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; @@ -69,6 +72,11 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientM } else { this.recipient = DcHelper.getContext(getContext()).getRecipient(contact); + String identifier = Hash.sha256(contact.getName() + contact.getAddr()); + Uri systemContactPhoto = TextSecurePreferences.getSystemContactPhoto(getContext(), identifier); + if (systemContactPhoto != null) { + this.recipient.setSystemContactPhoto(systemContactPhoto); + } this.recipient.addListener(this); if (this.recipient.getName() != null) { name = this.recipient.getName(); diff --git a/src/org/thoughtcrime/securesms/contacts/avatars/LocalFileContactPhoto.java b/src/org/thoughtcrime/securesms/contacts/avatars/LocalFileContactPhoto.java index 5ddefe60b..47b797679 100644 --- a/src/org/thoughtcrime/securesms/contacts/avatars/LocalFileContactPhoto.java +++ b/src/org/thoughtcrime/securesms/contacts/avatars/LocalFileContactPhoto.java @@ -35,7 +35,7 @@ public abstract class LocalFileContactPhoto implements ContactPhoto { @Override public @Nullable Uri getUri(@NonNull Context context) { - return isProfilePhoto() ? Uri.fromFile(AvatarHelper.getAvatarFile(context, address)) : null; + return isProfilePhoto() ? Uri.fromFile(AvatarHelper.getSelfAvatarFile(context, address)) : null; } @Override diff --git a/src/org/thoughtcrime/securesms/contacts/avatars/MyProfileContactPhoto.java b/src/org/thoughtcrime/securesms/contacts/avatars/MyProfileContactPhoto.java index 443500256..a637ce66e 100644 --- a/src/org/thoughtcrime/securesms/contacts/avatars/MyProfileContactPhoto.java +++ b/src/org/thoughtcrime/securesms/contacts/avatars/MyProfileContactPhoto.java @@ -30,7 +30,7 @@ public class MyProfileContactPhoto implements ContactPhoto { @Override public @Nullable Uri getUri(@NonNull Context context) { - return Uri.fromFile(AvatarHelper.getAvatarFile(context, address)); + return Uri.fromFile(AvatarHelper.getSelfAvatarFile(context, address)); } @Override diff --git a/src/org/thoughtcrime/securesms/contacts/avatars/SystemContactPhoto.java b/src/org/thoughtcrime/securesms/contacts/avatars/SystemContactPhoto.java index 1a16b9267..d88478729 100644 --- a/src/org/thoughtcrime/securesms/contacts/avatars/SystemContactPhoto.java +++ b/src/org/thoughtcrime/securesms/contacts/avatars/SystemContactPhoto.java @@ -3,13 +3,13 @@ package org.thoughtcrime.securesms.contacts.avatars; import android.content.Context; import android.net.Uri; +import android.provider.ContactsContract; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.util.Conversions; -import java.io.FileNotFoundException; import java.io.InputStream; import java.security.MessageDigest; @@ -26,8 +26,8 @@ public class SystemContactPhoto implements ContactPhoto { } @Override - public InputStream openInputStream(Context context) throws FileNotFoundException { - return context.getContentResolver().openInputStream(contactPhotoUri); + public InputStream openInputStream(Context context) { + return ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), contactPhotoUri, false); } @Nullable diff --git a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java index 3b47b517d..676a71c48 100644 --- a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java @@ -64,7 +64,7 @@ public class RetrieveProfileAvatarJob extends ContextJob implements InjectableTy if (TextUtils.isEmpty(profileAvatar)) { Log.w(TAG, "Removing profile avatar for: " + recipient.getAddress().serialize()); - AvatarHelper.delete(context, recipient.getAddress()); + AvatarHelper.deleteSelfAvatar(context, recipient.getAddress()); database.setProfileAvatar(recipient, profileAvatar); return; } @@ -76,7 +76,7 @@ public class RetrieveProfileAvatarJob extends ContextJob implements InjectableTy File decryptDestination = File.createTempFile("avatar", "jpg", context.getCacheDir()); Util.copy(avatarStream, new FileOutputStream(decryptDestination)); - decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.getAddress())); + decryptDestination.renameTo(AvatarHelper.getSelfAvatarFile(context, recipient.getAddress())); } finally { if (downloadDestination != null) downloadDestination.delete(); } diff --git a/src/org/thoughtcrime/securesms/profiles/AvatarHelper.java b/src/org/thoughtcrime/securesms/profiles/AvatarHelper.java index 9c00856a8..2635a0b45 100644 --- a/src/org/thoughtcrime/securesms/profiles/AvatarHelper.java +++ b/src/org/thoughtcrime/securesms/profiles/AvatarHelper.java @@ -3,16 +3,19 @@ package org.thoughtcrime.securesms.profiles; import android.annotation.SuppressLint; import android.content.Context; +import android.graphics.Bitmap; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.annimon.stream.Stream; +import com.b44t.messenger.DcContext; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.database.Address; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -37,19 +40,63 @@ public class AvatarHelper { return context.getFilesDir().getAbsolutePath() + File.separator + AVATAR_DIRECTORY + File.separator; } + public static void setGroupAvatar(Context context, int chatId, Bitmap bitmap) { + if (bitmap == null) { + return; + } + DcContext dcContext = DcHelper.getContext(context); + String avatarPath = dcContext.getChat(chatId).getProfileImage(); + if (avatarPath != null && !avatarPath.isEmpty()) { + File oldImage = new File(avatarPath); + if (oldImage.exists()) { + //noinspection ResultOfMethodCallIgnored + oldImage.delete(); + } + } + avatarPath = getFilePathForGroupAvatar(context, chatId, System.currentTimeMillis()); + FileOutputStream outStream; + try { + outStream = new FileOutputStream(avatarPath); + bitmap.compress(Bitmap.CompressFormat.JPEG, 85, outStream); + dcContext.setChatProfileImage(chatId, avatarPath); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + + @SuppressLint("DefaultLocale") public static String getFilePathForContactAvatar(Context context, int contactId, long timestamp) { return getAvatarDirectoryPath(context) + String.format(CONTACT_TEMPLATE, contactId, timestamp); } - public static InputStream getInputStreamFor(@NonNull Context context, @NonNull Address address) - throws IOException { - return new FileInputStream(getAvatarFile(context, address)); + public static void setContactAvatar(Context context, int contactId, Bitmap bitmap) { + if (bitmap == null) { + return; + } + DcContext dcContext = DcHelper.getContext(context); + String avatarPath = dcContext.getContact(contactId).getProfileImage(); + if (avatarPath != null && !avatarPath.isEmpty()) { + File oldImage = new File(avatarPath); + if (oldImage.exists()) { + //noinspection ResultOfMethodCallIgnored + oldImage.delete(); + } + } + avatarPath = getFilePathForContactAvatar(context, contactId, System.currentTimeMillis()); + FileOutputStream outStream; + try { + outStream = new FileOutputStream(avatarPath); + bitmap.compress(Bitmap.CompressFormat.JPEG, 85, outStream); + //dcContext.setContactProfileImage(contactId, avatarPath); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } } public static InputStream getInputStreamFor(@NonNull Context context, @NonNull String address) throws IOException { - return new FileInputStream(getAvatarFile(context, address)); + return new FileInputStream(getSelfAvatarFile(context, address)); } public static List getAvatarFiles(@NonNull Context context) { @@ -60,50 +107,50 @@ public class AvatarHelper { else return Stream.of(results).toList(); } - public static void delete(@NonNull Context context, @NonNull Address address) { - delete(context, getAvatarFile(context, address)); + public static void deleteSelfAvatar(@NonNull Context context, @NonNull Address address) { + deleteSelfAvatar(context, getSelfAvatarFile(context, address)); } - public static void delete(@NonNull Context context, @NonNull File avatar) { + public static void deleteSelfAvatar(@NonNull Context context, @NonNull File avatar) { avatar.delete(); DcHelper.set(context, DcHelper.CONFIG_SELF_AVATAR, null); } - public static @NonNull - File getAvatarFile(@NonNull Context context, @NonNull Address address) { + public static File getSelfAvatarFile(@NonNull Context context, @NonNull Address address) { String name = new File(address.serialize()).getName(); - return getAvatarFile(context, name); + return getSelfAvatarFile(context, name); } - public static @NonNull - File getAvatarFile(@NonNull Context context, @NonNull String address) { + public static File getSelfAvatarFile(@NonNull Context context, @NonNull String address) { File avatarDirectory = new File(context.getFilesDir(), AVATAR_DIRECTORY); avatarDirectory.mkdirs(); return new File(avatarDirectory, address); } - public static void setAvatar(@NonNull Context context, @NonNull Address address, @Nullable byte[] data) + public static void setSelfAvatar(@NonNull Context context, @NonNull Address address, @Nullable byte[] data) throws IOException { if (data == null) { - delete(context, address); + deleteSelfAvatar(context, address); } else { - File avatar = getAvatarFile(context, address); - writeAvatarFile(context, avatar, data); + File avatar = getSelfAvatarFile(context, address); + writeSelfAvatarFile(context, avatar, data); } } - public static void setAvatar(@NonNull Context context, @NonNull String address, @Nullable byte[] data) throws IOException { - File avatar = getAvatarFile(context, address); + public static void setSelfAvatar(@NonNull Context context, @NonNull String address, @Nullable byte[] data) throws IOException { + File avatar = getSelfAvatarFile(context, address); if (data == null) { - delete(context, avatar); + deleteSelfAvatar(context, avatar); } else { - writeAvatarFile(context, avatar, data); + writeSelfAvatarFile(context, avatar, data); } } - private static void writeAvatarFile(@NonNull Context context, @NonNull File avatar, @Nullable byte[] data) throws IOException { + private static void writeSelfAvatarFile(@NonNull Context context, @NonNull File avatar, @Nullable byte[] data) throws IOException { FileOutputStream out = new FileOutputStream(avatar); - out.write(data); + if (data != null) { + out.write(data); + } out.close(); String path = avatar.getPath(); DcHelper.set(context, DcHelper.CONFIG_SELF_AVATAR, path); diff --git a/src/org/thoughtcrime/securesms/util/Hash.java b/src/org/thoughtcrime/securesms/util/Hash.java new file mode 100644 index 000000000..f0de2c7fc --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/Hash.java @@ -0,0 +1,22 @@ +package org.thoughtcrime.securesms.util; + +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class Hash { + + public static String sha256(String input) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(input.getBytes(Charset.forName("UTF-8"))); + byte[] digest = messageDigest.digest(); + return String.format("%064x", new BigInteger(1, digest)); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return null; + } + +} diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index 163d86a31..1c6d5d117 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -1,11 +1,13 @@ package org.thoughtcrime.securesms.util; +import android.content.ContentUris; import android.content.Context; import android.content.SharedPreferences; import android.hardware.Camera.CameraInfo; import android.net.Uri; import android.os.Build; import android.preference.PreferenceManager; +import android.provider.ContactsContract; import android.provider.Settings; import android.support.annotation.ArrayRes; import android.support.annotation.NonNull; @@ -20,9 +22,11 @@ import org.whispersystems.libsignal.util.Medium; import java.io.IOException; import java.security.SecureRandom; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; public class TextSecurePreferences { @@ -134,6 +138,8 @@ public class TextSecurePreferences { private static final String LAST_FULL_CONTACT_SYNC_TIME = "pref_last_full_contact_sync_time"; private static final String NEEDS_FULL_CONTACT_SYNC = "pref_needs_full_contact_sync"; + private static final String PREF_CONTACT_PHOTO_IDENTIFIERS = "pref_contact_photo_identifiers"; + public static boolean isScreenLockEnabled(@NonNull Context context) { return getBooleanPreference(context, SCREEN_LOCK, false); } @@ -859,4 +865,21 @@ public class TextSecurePreferences { return defaultValues; } } + + public static void setSystemContactPhotos(Context context, Set contactPhotoIdentifiers) { + PreferenceManager.getDefaultSharedPreferences(context).edit().putStringSet(PREF_CONTACT_PHOTO_IDENTIFIERS, contactPhotoIdentifiers).apply(); + } + + public static Uri getSystemContactPhoto(Context context, String identifier) { + List contactPhotoIdentifiers = new ArrayList<>(getStringSetPreference(context, PREF_CONTACT_PHOTO_IDENTIFIERS, new HashSet<>())); + for(String contactPhotoIdentifier : contactPhotoIdentifiers) { + if (contactPhotoIdentifier.contains(identifier)) { + String[] parts = contactPhotoIdentifier.split("\\|"); + long contactId = Long.valueOf(parts[1]); + return ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId); + } + } + return null; + } + }