/* * Copyright (C) 2011 Whisper Systems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.thoughtcrime.securesms.recipients; import android.content.Context; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.text.TextUtils; import network.loki.messenger.R; import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; 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.util.ListenableFutureTask; import org.thoughtcrime.securesms.util.SoftHashMap; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; class RecipientProvider { @SuppressWarnings("unused") private static final String TAG = RecipientProvider.class.getSimpleName(); private static final RecipientCache recipientCache = new RecipientCache(); private static final ExecutorService asyncRecipientResolver = Util.newSingleThreadedLifoExecutor(); private static final Map STATIC_DETAILS = new HashMap() {{ put("262966", new RecipientDetails("Amazon", null, false, false, null, null)); }}; @NonNull Recipient getRecipient(@NonNull Context context, @NonNull Address address, @NonNull Optional settings, @NonNull Optional groupRecord, boolean asynchronous) { Recipient cachedRecipient = recipientCache.get(address); if (cachedRecipient != null && (asynchronous || !cachedRecipient.isResolving()) && ((!groupRecord.isPresent() && !settings.isPresent()) || !cachedRecipient.isResolving() || cachedRecipient.getName() != null)) { return cachedRecipient; } Optional prefetchedRecipientDetails = createPrefetchedRecipientDetails(context, address, settings, groupRecord); if (asynchronous) { cachedRecipient = new Recipient(context, address, cachedRecipient, prefetchedRecipientDetails, getRecipientDetailsAsync(context, address, settings, groupRecord)); } else { cachedRecipient = new Recipient(context, address, getRecipientDetailsSync(context, address, settings, groupRecord, false)); } recipientCache.set(address, cachedRecipient); return cachedRecipient; } @NonNull Optional getCached(@NonNull Address address) { return Optional.fromNullable(recipientCache.get(address)); } boolean removeCached(@NonNull Address address) { return recipientCache.remove(address); } private @NonNull Optional createPrefetchedRecipientDetails(@NonNull Context context, @NonNull Address address, @NonNull Optional settings, @NonNull Optional groupRecord) { if (address.isGroup() && settings.isPresent() && groupRecord.isPresent()) { return Optional.of(getGroupRecipientDetails(context, address, groupRecord, settings, true)); } else if (!address.isGroup() && settings.isPresent()) { boolean isLocalNumber = address.serialize().equals(TextSecurePreferences.getLocalNumber(context)); return Optional.of(new RecipientDetails(null, null, !TextUtils.isEmpty(settings.get().getSystemDisplayName()), isLocalNumber, settings.get(), null)); } return Optional.absent(); } private @NonNull ListenableFutureTask getRecipientDetailsAsync(final Context context, final @NonNull Address address, final @NonNull Optional settings, final @NonNull Optional groupRecord) { Callable task = () -> getRecipientDetailsSync(context, address, settings, groupRecord, true); ListenableFutureTask future = new ListenableFutureTask<>(task); asyncRecipientResolver.submit(future); return future; } private @NonNull RecipientDetails getRecipientDetailsSync(Context context, @NonNull Address address, Optional settings, Optional groupRecord, boolean nestedAsynchronous) { if (address.isGroup()) return getGroupRecipientDetails(context, address, groupRecord, settings, nestedAsynchronous); else return getIndividualRecipientDetails(context, address, settings); } private @NonNull RecipientDetails getIndividualRecipientDetails(Context context, @NonNull Address address, Optional settings) { if (!settings.isPresent()) { settings = DatabaseFactory.getRecipientDatabase(context).getRecipientSettings(address); } if (!settings.isPresent() && STATIC_DETAILS.containsKey(address.serialize())) { return STATIC_DETAILS.get(address.serialize()); } else { boolean systemContact = settings.isPresent() && !TextUtils.isEmpty(settings.get().getSystemDisplayName()); boolean isLocalNumber = address.serialize().equals(TextSecurePreferences.getLocalNumber(context)); return new RecipientDetails(null, null, systemContact, isLocalNumber, settings.orNull(), null); } } private @NonNull RecipientDetails getGroupRecipientDetails(Context context, Address groupId, Optional groupRecord, Optional settings, boolean asynchronous) { if (!groupRecord.isPresent()) { groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId.toGroupString()); } if (!settings.isPresent()) { settings = DatabaseFactory.getRecipientDatabase(context).getRecipientSettings(groupId); } if (groupRecord.isPresent()) { String title = groupRecord.get().getTitle(); List
memberAddresses = groupRecord.get().getMembers(); List members = new LinkedList<>(); Long avatarId = null; for (Address memberAddress : memberAddresses) { members.add(getRecipient(context, memberAddress, Optional.absent(), Optional.absent(), asynchronous)); } if (!groupId.isMmsGroup() && title == null) { title = context.getString(R.string.RecipientProvider_unnamed_group); } if (groupRecord.get().getAvatar() != null && groupRecord.get().getAvatar().length > 0) { avatarId = groupRecord.get().getAvatarId(); } return new RecipientDetails(title, avatarId, false, false, settings.orNull(), members); } return new RecipientDetails(context.getString(R.string.RecipientProvider_unnamed_group), null, false, false, settings.orNull(), null); } static class RecipientDetails { @Nullable final String name; @Nullable final String customLabel; @Nullable final Uri systemContactPhoto; @Nullable final Uri contactUri; @Nullable final Long groupAvatarId; @Nullable final MaterialColor color; @Nullable final Uri messageRingtone; @Nullable final Uri callRingtone; final long mutedUntil; @Nullable final VibrateState messageVibrateState; @Nullable final VibrateState callVibrateState; final boolean blocked; final int expireMessages; @NonNull final List participants; @Nullable final String profileName; final boolean seenInviteReminder; final Optional defaultSubscriptionId; @NonNull final RegisteredState registered; @Nullable final byte[] profileKey; @Nullable final String profileAvatar; final boolean profileSharing; final boolean systemContact; final boolean isLocalNumber; @Nullable final String notificationChannel; @NonNull final UnidentifiedAccessMode unidentifiedAccessMode; final boolean forceSmsSelection; RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId, boolean systemContact, boolean isLocalNumber, @Nullable RecipientSettings settings, @Nullable List participants) { this.groupAvatarId = groupAvatarId; this.systemContactPhoto = settings != null ? Util.uri(settings.getSystemContactPhotoUri()) : null; this.customLabel = settings != null ? settings.getSystemPhoneLabel() : null; this.contactUri = settings != null ? Util.uri(settings.getSystemContactUri()) : null; this.color = settings != null ? settings.getColor() : null; this.messageRingtone = settings != null ? settings.getMessageRingtone() : null; this.callRingtone = settings != null ? settings.getCallRingtone() : null; this.mutedUntil = settings != null ? settings.getMuteUntil() : 0; this.messageVibrateState = settings != null ? settings.getMessageVibrateState() : null; this.callVibrateState = settings != null ? settings.getCallVibrateState() : null; this.blocked = settings != null && settings.isBlocked(); this.expireMessages = settings != null ? settings.getExpireMessages() : 0; this.participants = participants == null ? new LinkedList<>() : participants; this.profileName = settings != null ? settings.getProfileName() : null; this.seenInviteReminder = settings != null && settings.hasSeenInviteReminder(); this.defaultSubscriptionId = settings != null ? settings.getDefaultSubscriptionId() : Optional.absent(); this.registered = settings != null ? settings.getRegistered() : RegisteredState.UNKNOWN; this.profileKey = settings != null ? settings.getProfileKey() : null; this.profileAvatar = settings != null ? settings.getProfileAvatar() : null; this.profileSharing = settings != null && settings.isProfileSharing(); this.systemContact = systemContact; this.isLocalNumber = isLocalNumber; this.notificationChannel = settings != null ? settings.getNotificationChannel() : null; this.unidentifiedAccessMode = settings != null ? settings.getUnidentifiedAccessMode() : UnidentifiedAccessMode.DISABLED; this.forceSmsSelection = settings != null && settings.isForceSmsSelection(); if (name == null && settings != null) this.name = settings.getSystemDisplayName(); else this.name = name; } } private static class RecipientCache { private final Map cache = new SoftHashMap<>(1000); public synchronized Recipient get(Address address) { return cache.get(address); } public synchronized void set(Address address, Recipient recipient) { cache.put(address, recipient); } public synchronized boolean remove(Address address) { return cache.remove(address) != null; } } }