Reduce profile avatar disk reads.

This commit is contained in:
Cody Henthorne 2022-07-13 14:48:25 -04:00
parent 2f17963b2b
commit 819f7a170f
16 changed files with 81 additions and 59 deletions

View file

@ -92,7 +92,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
Recipient.live(recipientId).observe(this, recipient -> {
ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(recipient)
: recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.isSelf() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
: recipient.getFallbackContactPhoto();

View file

@ -160,8 +160,7 @@ public final class AvatarImageView extends AppCompatImageView {
private void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
if (recipient != null) {
RecipientContactPhoto photo = (recipient.isSelf() && avatarOptions.useSelfProfileAvatar) ? new RecipientContactPhoto(recipient,
new ProfileContactPhoto(Recipient.self(),
Recipient.self().getProfileAvatar()))
new ProfileContactPhoto(Recipient.self()))
: new RecipientContactPhoto(recipient);
boolean shouldBlur = recipient.shouldBlurAvatar();

View file

@ -253,7 +253,7 @@ public class CallParticipantView extends ConstraintLayout {
}
private void setPipAvatar(@NonNull Recipient recipient) {
ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(Recipient.self(), Recipient.self().getProfileAvatar())
ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(Recipient.self())
: recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);

View file

@ -505,7 +505,7 @@ public class WebRtcCallView extends ConstraintLayout {
largeLocalRenderNoVideoAvatar.setVisibility(View.VISIBLE);
GlideApp.with(getContext().getApplicationContext())
.load(new ProfileContactPhoto(localCallParticipant.getRecipient(), localCallParticipant.getRecipient().getProfileAvatar()))
.load(new ProfileContactPhoto(localCallParticipant.getRecipient()))
.transform(new CenterCrop(), new BlurTransformation(getContext(), 0.25f, BlurTransformation.MAX_RADIUS))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(largeLocalRenderNoVideoAvatar);

View file

@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.libsignal.protocol.util.ByteUtil;
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -19,12 +20,14 @@ import java.util.Objects;
public class ProfileContactPhoto implements ContactPhoto {
private final @NonNull Recipient recipient;
private final @NonNull String avatarObject;
private final @NonNull Recipient recipient;
private final @NonNull String avatarObject;
private final @NonNull ProfileAvatarFileDetails profileAvatarFileDetails;
public ProfileContactPhoto(@NonNull Recipient recipient, @Nullable String avatarObject) {
this.recipient = recipient;
this.avatarObject = avatarObject == null ? "" : avatarObject;
public ProfileContactPhoto(@NonNull Recipient recipient) {
this.recipient = recipient;
this.avatarObject = recipient.getProfileAvatar() == null ? "" : recipient.getProfileAvatar();
this.profileAvatarFileDetails = recipient.getProfileAvatarFileDetails();
}
@Override
@ -46,7 +49,7 @@ public class ProfileContactPhoto implements ContactPhoto {
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(recipient.getId().serialize().getBytes());
messageDigest.update(avatarObject.getBytes());
messageDigest.update(ByteUtil.longToByteArray(getFileLastModified()));
messageDigest.update(profileAvatarFileDetails.getDiskCacheKeyBytes());
}
@Override
@ -55,16 +58,12 @@ public class ProfileContactPhoto implements ContactPhoto {
if (o == null || getClass() != o.getClass()) return false;
ProfileContactPhoto that = (ProfileContactPhoto) o;
return recipient.equals(that.recipient) &&
avatarObject.equals(that.avatarObject) &&
getFileLastModified() == that.getFileLastModified();
avatarObject.equals(that.avatarObject) &&
profileAvatarFileDetails.equals(that.profileAvatarFileDetails);
}
@Override
public int hashCode() {
return Objects.hash(recipient, avatarObject, getFileLastModified());
}
private long getFileLastModified() {
return AvatarHelper.getLastModified(ApplicationDependencies.getApplication(), recipient.getId());
return Objects.hash(recipient, avatarObject, profileAvatarFileDetails);
}
}

View file

@ -3609,7 +3609,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
systemContactUri = cursor.requireString(SYSTEM_CONTACT_URI),
signalProfileName = ProfileName.fromParts(cursor.requireString(PROFILE_GIVEN_NAME), cursor.requireString(PROFILE_FAMILY_NAME)),
signalProfileAvatar = cursor.requireString(SIGNAL_PROFILE_AVATAR),
hasProfileImage = AvatarHelper.hasAvatar(context, recipientId),
profileAvatarFileDetails = AvatarHelper.getAvatarFileDetails(context, recipientId),
profileSharing = cursor.requireBoolean(PROFILE_SHARING),
lastProfileFetch = cursor.requireLong(LAST_PROFILE_FETCH),
notificationChannel = cursor.requireString(NOTIFICATION_CHANNEL),

View file

@ -0,0 +1,23 @@
package org.thoughtcrime.securesms.database.model
/**
* Details related to the current avatar profile image file that would be returned via [org.thoughtcrime.securesms.profiles.AvatarHelper.getAvatarFile]
* at the time this [org.thoughtcrime.securesms.recipients.Recipient] was loaded/refreshed from the database.
*/
data class ProfileAvatarFileDetails(
val hashId: Long,
val lastModified: Long
) {
fun getDiskCacheKeyBytes(): ByteArray {
return toString().toByteArray()
}
fun hasFile(): Boolean {
return this != NO_DETAILS
}
companion object {
@JvmField
val NO_DETAILS = ProfileAvatarFileDetails(0, 0)
}
}

View file

@ -55,8 +55,7 @@ data class RecipientRecord(
val signalProfileName: ProfileName,
@get:JvmName("getProfileAvatar")
val signalProfileAvatar: String?,
@get:JvmName("hasProfileImage")
val hasProfileImage: Boolean,
val profileAvatarFileDetails: ProfileAvatarFileDetails,
@get:JvmName("isProfileSharing")
val profileSharing: Boolean,
val lastProfileFetch: Long,

View file

@ -65,7 +65,7 @@ public class InsightsRepository implements InsightsDashboardViewModel.Repository
Recipient self = Recipient.self().resolve();
String name = Optional.of(self.getDisplayName(context)).orElse("");
return new InsightsUserAvatar(new ProfileContactPhoto(self, self.getProfileAvatar()),
return new InsightsUserAvatar(new ProfileContactPhoto(self),
self.getAvatarColor(),
new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40));
}, avatarConsumer::accept);

View file

@ -36,7 +36,7 @@ fun Drawable?.toLargeBitmap(context: Context): Bitmap? {
}
fun Recipient.getContactDrawable(context: Context): Drawable? {
val contactPhoto: ContactPhoto? = if (isSelf) ProfileContactPhoto(this, profileAvatar) else contactPhoto
val contactPhoto: ContactPhoto? = if (isSelf) ProfileContactPhoto(this) else contactPhoto
val fallbackContactPhoto: FallbackContactPhoto = if (isSelf) getFallback(context) else fallbackContactPhoto
return if (contactPhoto != null) {
try {

View file

@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@ -103,6 +104,14 @@ public class AvatarHelper {
return avatarFile.exists() && avatarFile.length() > 0;
}
public static @NonNull ProfileAvatarFileDetails getAvatarFileDetails(@NonNull Context context, @NonNull RecipientId recipientId) {
File avatarFile = getAvatarFile(context, recipientId);
if (avatarFile.exists() && avatarFile.length() > 0) {
return new ProfileAvatarFileDetails(avatarFile.hashCode(), avatarFile.lastModified());
}
return ProfileAvatarFileDetails.NO_DETAILS;
}
/**
* Retrieves a stream for an avatar. If there is no avatar, the stream will likely throw an
* IOException. It is recommended to call {@link #hasAvatar(Context, RecipientId)} first.
@ -174,19 +183,6 @@ public class AvatarHelper {
return ModernEncryptingPartOutputStream.createFor(attachmentSecret, targetFile, true).second;
}
/**
* Returns the timestamp of when the avatar was last modified, or zero if the avatar doesn't exist.
*/
public static long getLastModified(@NonNull Context context, @NonNull RecipientId recipientId) {
File file = getAvatarFile(context, recipientId);
if (file.exists()) {
return file.lastModified();
} else {
return 0;
}
}
/**
* Returns a {@link StreamDetails} for the local user's own avatar, or null if one does not exist.
*/

View file

@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessM
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListId;
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails;
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
@ -110,7 +111,7 @@ public class Recipient {
private final Uri contactUri;
private final ProfileName signalProfileName;
private final String profileAvatar;
private final boolean hasProfileImage;
private final ProfileAvatarFileDetails profileAvatarFileDetails;
private final boolean profileSharing;
private final long lastProfileFetch;
private final String notificationChannel;
@ -388,7 +389,7 @@ public class Recipient {
this.contactUri = null;
this.signalProfileName = ProfileName.EMPTY;
this.profileAvatar = null;
this.hasProfileImage = false;
this.profileAvatarFileDetails = ProfileAvatarFileDetails.NO_DETAILS;
this.profileSharing = false;
this.lastProfileFetch = 0;
this.notificationChannel = null;
@ -446,7 +447,7 @@ public class Recipient {
this.contactUri = details.contactUri;
this.signalProfileName = details.profileName;
this.profileAvatar = details.profileAvatar;
this.hasProfileImage = details.hasProfileImage;
this.profileAvatarFileDetails = details.profileAvatarFileDetails;
this.profileSharing = details.profileSharing;
this.lastProfileFetch = details.lastProfileFetch;
this.notificationChannel = details.notificationChannel;
@ -811,6 +812,10 @@ public class Recipient {
return profileAvatar;
}
public @NonNull ProfileAvatarFileDetails getProfileAvatarFileDetails() {
return profileAvatarFileDetails;
}
public boolean isProfileSharing() {
return profileSharing;
}
@ -907,7 +912,7 @@ public class Recipient {
if (isSelf) return null;
else if (isGroupInternal() && groupAvatarId.isPresent()) return new GroupRecordContactPhoto(groupId, groupAvatarId.get());
else if (systemContactPhoto != null && SignalStore.settings().isPreferSystemContactPhotos()) return new SystemContactPhoto(id, systemContactPhoto, 0);
else if (profileAvatar != null && hasProfileImage) return new ProfileContactPhoto(this, profileAvatar);
else if (profileAvatar != null && profileAvatarFileDetails.hasFile()) return new ProfileContactPhoto(this);
else if (systemContactPhoto != null) return new SystemContactPhoto(id, systemContactPhoto, 0);
else return null;
}
@ -1263,7 +1268,7 @@ public class Recipient {
blocked == other.blocked &&
muteUntil == other.muteUntil &&
expireMessages == other.expireMessages &&
hasProfileImage == other.hasProfileImage &&
Objects.equals(profileAvatarFileDetails, other.profileAvatarFileDetails) &&
profileSharing == other.profileSharing &&
lastProfileFetch == other.lastProfileFetch &&
forceSmsSelection == other.forceSmsSelection &&

View file

@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.database.model.DistributionListId;
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails;
import org.thoughtcrime.securesms.database.model.RecipientRecord;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@ -60,7 +61,7 @@ public class RecipientDetails {
final byte[] profileKey;
final ExpiringProfileKeyCredential expiringProfileKeyCredential;
final String profileAvatar;
final boolean hasProfileImage;
final ProfileAvatarFileDetails profileAvatarFileDetails;
final boolean profileSharing;
final long lastProfileFetch;
final boolean systemContact;
@ -123,7 +124,7 @@ public class RecipientDetails {
this.profileKey = record.getProfileKey();
this.expiringProfileKeyCredential = record.getExpiringProfileKeyCredential();
this.profileAvatar = record.getProfileAvatar();
this.hasProfileImage = record.hasProfileImage();
this.profileAvatarFileDetails = record.getProfileAvatarFileDetails();
this.profileSharing = record.isProfileSharing();
this.lastProfileFetch = record.getLastProfileFetch();
this.systemContact = systemContact;
@ -181,7 +182,7 @@ public class RecipientDetails {
this.profileKey = null;
this.expiringProfileKeyCredential = null;
this.profileAvatar = null;
this.hasProfileImage = false;
this.profileAvatarFileDetails = ProfileAvatarFileDetails.NO_DETAILS;
this.profileSharing = false;
this.lastProfileFetch = 0;
this.systemContact = true;

View file

@ -44,7 +44,7 @@ public final class AvatarUtil {
ContactPhoto photo;
if (recipient.isSelf()) {
photo = new ProfileContactPhoto(Recipient.self(), Recipient.self().getProfileAvatar());
photo = new ProfileContactPhoto(Recipient.self());
} else if (recipient.getContactPhoto() == null) {
target.setImageDrawable(null);
target.setBackgroundColor(ContextCompat.getColor(target.getContext(), R.color.black));
@ -150,7 +150,7 @@ public final class AvatarUtil {
private static <T> GlideRequest<T> request(@NonNull GlideRequest<T> glideRequest, @NonNull Context context, @NonNull Recipient recipient, boolean loadSelf, int targetSize) {
final ContactPhoto photo;
if (Recipient.self().equals(recipient) && loadSelf) {
photo = new ProfileContactPhoto(recipient, recipient.getProfileAvatar());
photo = new ProfileContactPhoto(recipient);
} else {
photo = recipient.getContactPhoto();
}

View file

@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto;
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -41,13 +42,15 @@ public final class ConversationShortcutPhoto implements Key {
*/
private static final long VERSION = 1L;
private final Recipient recipient;
private final String avatarObject;
private final Recipient recipient;
private final String avatarObject;
private final ProfileAvatarFileDetails profileAvatarFileDetails;
@WorkerThread
public ConversationShortcutPhoto(@NonNull Recipient recipient) {
this.recipient = recipient.resolve();
this.avatarObject = Util.firstNonNull(recipient.getProfileAvatar(), "");
this.recipient = recipient.resolve();
this.avatarObject = Util.firstNonNull(recipient.getProfileAvatar(), "");
this.profileAvatarFileDetails = recipient.getProfileAvatarFileDetails();
}
@Override
@ -55,7 +58,7 @@ public final class ConversationShortcutPhoto implements Key {
messageDigest.update(recipient.getDisplayName(ApplicationDependencies.getApplication()).getBytes());
messageDigest.update(avatarObject.getBytes());
messageDigest.update(isSystemContactPhoto() ? (byte) 1 : (byte) 0);
messageDigest.update(ByteUtil.longToByteArray(getFileLastModified()));
messageDigest.update(profileAvatarFileDetails.getDiskCacheKeyBytes());
messageDigest.update(ByteUtil.longToByteArray(VERSION));
}
@ -67,22 +70,18 @@ public final class ConversationShortcutPhoto implements Key {
return Objects.equals(recipient, that.recipient) &&
Objects.equals(avatarObject, that.avatarObject) &&
isSystemContactPhoto() == that.isSystemContactPhoto() &&
getFileLastModified() == that.getFileLastModified();
Objects.equals(profileAvatarFileDetails, that.profileAvatarFileDetails);
}
@Override
public int hashCode() {
return Objects.hash(recipient, avatarObject, isSystemContactPhoto(), getFileLastModified());
return Objects.hash(recipient, avatarObject, isSystemContactPhoto(), profileAvatarFileDetails);
}
private boolean isSystemContactPhoto() {
return recipient.getContactPhoto() instanceof SystemContactPhoto;
}
private long getFileLastModified() {
return AvatarHelper.getLastModified(ApplicationDependencies.getApplication(), recipient.getId());
}
public static final class Loader implements ModelLoader<ConversationShortcutPhoto, Bitmap> {
private final Context context;

View file

@ -6,6 +6,7 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails
import org.thoughtcrime.securesms.database.model.RecipientRecord
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.profiles.ProfileName
@ -55,7 +56,7 @@ object RecipientDatabaseTestUtils {
systemContactUri: String? = null,
signalProfileName: ProfileName = ProfileName.EMPTY,
signalProfileAvatar: String? = null,
hasProfileImage: Boolean = false,
profileAvatarFileDetails: ProfileAvatarFileDetails = ProfileAvatarFileDetails.NO_DETAILS,
profileSharing: Boolean = false,
lastProfileFetch: Long = 0L,
notificationChannel: String? = null,
@ -119,7 +120,7 @@ object RecipientDatabaseTestUtils {
systemContactUri,
signalProfileName,
signalProfileAvatar,
hasProfileImage,
profileAvatarFileDetails,
profileSharing,
lastProfileFetch,
notificationChannel,