mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 19:29:54 +01:00
Move all files to natural position.
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class LiveRecipient {
|
||||
|
||||
private static final String TAG = Log.tag(LiveRecipient.class);
|
||||
|
||||
private final Context context;
|
||||
private final MutableLiveData<Recipient> liveData;
|
||||
private final Set<RecipientForeverObserver> observers;
|
||||
private final Observer<Recipient> foreverObserver;
|
||||
private final AtomicReference<Recipient> recipient;
|
||||
private final RecipientDatabase recipientDatabase;
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final String unnamedGroupName;
|
||||
|
||||
LiveRecipient(@NonNull Context context, @NonNull MutableLiveData<Recipient> liveData, @NonNull Recipient defaultRecipient) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.liveData = liveData;
|
||||
this.recipient = new AtomicReference<>(defaultRecipient);
|
||||
this.recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
this.unnamedGroupName = context.getString(R.string.RecipientProvider_unnamed_group);
|
||||
this.observers = new CopyOnWriteArraySet<>();
|
||||
this.foreverObserver = recipient -> {
|
||||
for (RecipientForeverObserver o : observers) {
|
||||
o.onRecipientChanged(recipient);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getId() {
|
||||
return recipient.get().getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A recipient that may or may not be fully-resolved.
|
||||
*/
|
||||
public @NonNull Recipient get() {
|
||||
return recipient.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch the recipient for changes. The callback will only be invoked if the provided lifecycle is
|
||||
* in a valid state. No need to remove the observer. If you do wish to remove the observer (if,
|
||||
* for instance, you wish to remove the listener before the end of the owner's lifecycle), you can
|
||||
* use {@link #removeObservers(LifecycleOwner)}.
|
||||
*/
|
||||
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<Recipient> observer) {
|
||||
Util.postToMain(() -> liveData.observe(owner, observer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all observers of this data registered for the given LifecycleOwner.
|
||||
*/
|
||||
public void removeObservers(@NonNull LifecycleOwner owner) {
|
||||
Util.runOnMain(() -> liveData.removeObservers(owner));
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch the recipient for changes. The callback could be invoked at any time. You MUST call
|
||||
* {@link #removeForeverObserver(RecipientForeverObserver)} when finished. You should use
|
||||
* {@link #observe(LifecycleOwner, Observer<Recipient>)} if possible, as it is lifecycle-safe.
|
||||
*/
|
||||
public void observeForever(@NonNull RecipientForeverObserver observer) {
|
||||
Util.postToMain(() -> {
|
||||
observers.add(observer);
|
||||
|
||||
if (observers.size() == 1) {
|
||||
liveData.observeForever(foreverObserver);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes the provided {@link RecipientForeverObserver} from future changes.
|
||||
*/
|
||||
public void removeForeverObserver(@NonNull RecipientForeverObserver observer) {
|
||||
Util.postToMain(() -> {
|
||||
observers.remove(observer);
|
||||
|
||||
if (observers.isEmpty()) {
|
||||
liveData.removeObserver(foreverObserver);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A fully-resolved version of the recipient. May require reading from disk.
|
||||
*/
|
||||
@WorkerThread
|
||||
public @NonNull Recipient resolve() {
|
||||
Recipient current = recipient.get();
|
||||
|
||||
if (!current.isResolving() || current.getId().isUnknown()) {
|
||||
return current;
|
||||
}
|
||||
|
||||
if (Util.isMainThread()) {
|
||||
Log.w(TAG, "[Resolve][MAIN] " + getId(), new Throwable());
|
||||
}
|
||||
|
||||
Recipient updated = fetchRecipientFromDisk(getId());
|
||||
List<Recipient> participants = Stream.of(updated.getParticipants())
|
||||
.filter(Recipient::isResolving)
|
||||
.map(Recipient::getId)
|
||||
.map(this::fetchRecipientFromDisk)
|
||||
.toList();
|
||||
|
||||
for (Recipient participant : participants) {
|
||||
participant.live().set(participant);
|
||||
}
|
||||
|
||||
set(updated);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces a reload of the underlying recipient.
|
||||
*/
|
||||
@WorkerThread
|
||||
public void refresh() {
|
||||
if (getId().isUnknown()) return;
|
||||
|
||||
if (Util.isMainThread()) {
|
||||
Log.w(TAG, "[Refresh][MAIN] " + getId(), new Throwable());
|
||||
}
|
||||
|
||||
Recipient recipient = fetchRecipientFromDisk(getId());
|
||||
List<Recipient> participants = Stream.of(recipient.getParticipants())
|
||||
.map(Recipient::getId)
|
||||
.map(this::fetchRecipientFromDisk)
|
||||
.toList();
|
||||
|
||||
for (Recipient participant : participants) {
|
||||
participant.live().set(participant);
|
||||
}
|
||||
|
||||
set(recipient);
|
||||
}
|
||||
|
||||
public @NonNull LiveData<Recipient> getLiveData() {
|
||||
return liveData;
|
||||
}
|
||||
|
||||
private @NonNull Recipient fetchRecipientFromDisk(RecipientId id) {
|
||||
RecipientSettings settings = recipientDatabase.getRecipientSettings(id);
|
||||
RecipientDetails details = settings.getGroupId() != null ? getGroupRecipientDetails(settings)
|
||||
: getIndividualRecipientDetails(settings);
|
||||
|
||||
return new Recipient(id, details);
|
||||
}
|
||||
|
||||
private @NonNull RecipientDetails getIndividualRecipientDetails(RecipientSettings settings) {
|
||||
boolean systemContact = !TextUtils.isEmpty(settings.getSystemDisplayName());
|
||||
boolean isLocalNumber = (settings.getE164() != null && settings.getE164().equals(TextSecurePreferences.getLocalNumber(context))) ||
|
||||
(settings.getUuid() != null && settings.getUuid().equals(TextSecurePreferences.getLocalUuid(context)));
|
||||
|
||||
return new RecipientDetails(context, null, Optional.absent(), systemContact, isLocalNumber, settings, null);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull RecipientDetails getGroupRecipientDetails(@NonNull RecipientSettings settings) {
|
||||
Optional<GroupRecord> groupRecord = groupDatabase.getGroup(settings.getId());
|
||||
|
||||
if (groupRecord.isPresent()) {
|
||||
String title = groupRecord.get().getTitle();
|
||||
List<Recipient> members = Stream.of(groupRecord.get().getMembers()).filterNot(RecipientId::isUnknown).map(this::fetchRecipientFromDisk).toList();
|
||||
Optional<Long> avatarId = Optional.absent();
|
||||
|
||||
if (settings.getGroupId() != null && !GroupUtil.isMmsGroup(settings.getGroupId()) && title == null) {
|
||||
title = unnamedGroupName;
|
||||
}
|
||||
|
||||
if (groupRecord.get().getAvatar() != null && groupRecord.get().getAvatar().length > 0) {
|
||||
avatarId = Optional.of(groupRecord.get().getAvatarId());
|
||||
}
|
||||
|
||||
return new RecipientDetails(context, title, avatarId, false, false, settings, members);
|
||||
}
|
||||
|
||||
return new RecipientDetails(context, unnamedGroupName, Optional.absent(), false, false, settings, null);
|
||||
}
|
||||
|
||||
private synchronized void set(@NonNull Recipient recipient) {
|
||||
this.recipient.set(recipient);
|
||||
this.liveData.postValue(recipient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
LiveRecipient that = (LiveRecipient) o;
|
||||
return recipient.equals(that.recipient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(recipient);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.MissingRecipientError;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.LRUCache;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class LiveRecipientCache {
|
||||
|
||||
private static final String TAG = Log.tag(LiveRecipientCache.class);
|
||||
|
||||
private static final int CACHE_MAX = 1000;
|
||||
private static final int CACHE_WARM_MAX = 500;
|
||||
|
||||
private final Context context;
|
||||
private final RecipientDatabase recipientDatabase;
|
||||
private final Map<RecipientId, LiveRecipient> recipients;
|
||||
private final LiveRecipient unknown;
|
||||
|
||||
private RecipientId localRecipientId;
|
||||
private boolean warmedUp;
|
||||
|
||||
@SuppressLint("UseSparseArrays")
|
||||
public LiveRecipientCache(@NonNull Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
this.recipients = new LRUCache<>(CACHE_MAX);
|
||||
this.unknown = new LiveRecipient(context, new MutableLiveData<>(), Recipient.UNKNOWN);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
synchronized @NonNull LiveRecipient getLive(@NonNull RecipientId id) {
|
||||
if (id.isUnknown()) return unknown;
|
||||
|
||||
LiveRecipient live = recipients.get(id);
|
||||
|
||||
if (live == null) {
|
||||
final LiveRecipient newLive = new LiveRecipient(context, new MutableLiveData<>(), new Recipient(id));
|
||||
|
||||
recipients.put(id, newLive);
|
||||
|
||||
MissingRecipientError prettyStackTraceError = new MissingRecipientError(newLive.getId());
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
newLive.resolve();
|
||||
} catch (MissingRecipientError e) {
|
||||
throw prettyStackTraceError;
|
||||
}
|
||||
});
|
||||
|
||||
live = newLive;
|
||||
}
|
||||
|
||||
return live;
|
||||
}
|
||||
|
||||
@NonNull Recipient getSelf() {
|
||||
synchronized (this) {
|
||||
if (localRecipientId == null) {
|
||||
UUID localUuid = TextSecurePreferences.getLocalUuid(context);
|
||||
String localE164 = TextSecurePreferences.getLocalNumber(context);
|
||||
|
||||
if (localUuid != null) {
|
||||
localRecipientId = recipientDatabase.getByUuid(localUuid).or(recipientDatabase.getByE164(localE164)).orNull();
|
||||
} else if (localE164 != null) {
|
||||
localRecipientId = recipientDatabase.getByE164(localE164).orNull();
|
||||
} else {
|
||||
throw new AssertionError("Tried to call getSelf() before local data was set!");
|
||||
}
|
||||
|
||||
if (localRecipientId == null) {
|
||||
throw new MissingRecipientError(localRecipientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return getLive(localRecipientId).resolve();
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public synchronized void warmUp() {
|
||||
if (warmedUp) {
|
||||
return;
|
||||
} else {
|
||||
warmedUp = true;
|
||||
}
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
|
||||
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(threadDatabase.getConversationList())) {
|
||||
int i = 0;
|
||||
ThreadRecord record = null;
|
||||
List<Recipient> recipients = new ArrayList<>();
|
||||
|
||||
while ((record = reader.getNext()) != null && i < CACHE_WARM_MAX) {
|
||||
recipients.add(record.getRecipient());
|
||||
i++;
|
||||
}
|
||||
|
||||
Log.d(TAG, "Warming up " + recipients.size() + " recipients.");
|
||||
|
||||
Collections.reverse(recipients);
|
||||
Stream.of(recipients).map(Recipient::getId).forEach(this::getLive);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public synchronized void clear() {
|
||||
recipients.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,725 @@
|
||||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GroupRecordContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.TransparentContactPhoto;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
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.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
|
||||
|
||||
public class Recipient {
|
||||
|
||||
public static final Recipient UNKNOWN = new Recipient(RecipientId.UNKNOWN, new RecipientDetails());
|
||||
|
||||
private static final String TAG = Log.tag(Recipient.class);
|
||||
|
||||
private final RecipientId id;
|
||||
private final boolean resolving;
|
||||
private final UUID uuid;
|
||||
private final String username;
|
||||
private final String e164;
|
||||
private final String email;
|
||||
private final String groupId;
|
||||
private final List<Recipient> participants;
|
||||
private final Optional<Long> groupAvatarId;
|
||||
private final boolean localNumber;
|
||||
private final boolean blocked;
|
||||
private final long muteUntil;
|
||||
private final VibrateState messageVibrate;
|
||||
private final VibrateState callVibrate;
|
||||
private final Uri messageRingtone;
|
||||
private final Uri callRingtone;
|
||||
private final MaterialColor color;
|
||||
private final Optional<Integer> defaultSubscriptionId;
|
||||
private final int expireMessages;
|
||||
private final RegisteredState registered;
|
||||
private final byte[] profileKey;
|
||||
private final String name;
|
||||
private final Uri systemContactPhoto;
|
||||
private final String customLabel;
|
||||
private final Uri contactUri;
|
||||
private final String profileName;
|
||||
private final String profileAvatar;
|
||||
private final boolean profileSharing;
|
||||
private final String notificationChannel;
|
||||
private final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
private final boolean forceSmsSelection;
|
||||
private final boolean uuidSupported;
|
||||
private final InsightsBannerTier insightsBannerTier;
|
||||
private final byte[] storageKey;
|
||||
private final byte[] identityKey;
|
||||
private final VerifiedStatus identityStatus;
|
||||
|
||||
|
||||
/**
|
||||
* Returns a {@link LiveRecipient}, which contains a {@link Recipient} that may or may not be
|
||||
* populated with data. However, you can observe the value that's returned to be notified when the
|
||||
* {@link Recipient} changes.
|
||||
*/
|
||||
@AnyThread
|
||||
public static @NonNull LiveRecipient live(@NonNull RecipientId id) {
|
||||
Preconditions.checkNotNull(id, "ID cannot be null.");
|
||||
return ApplicationDependencies.getRecipientCache().getLive(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fully-populated {@link Recipient}. May hit the disk, and therefore should be
|
||||
* called on a background thread.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull Recipient resolved(@NonNull RecipientId id) {
|
||||
Preconditions.checkNotNull(id, "ID cannot be null.");
|
||||
return live(id).resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fully-populated {@link Recipient} and associates it with the provided username.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull Recipient externalUsername(@NonNull Context context, @NonNull UUID uuid, @NonNull String username) {
|
||||
Recipient recipient = externalPush(context, uuid, null);
|
||||
DatabaseFactory.getRecipientDatabase(context).setUsername(recipient.getId(), username);
|
||||
return recipient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fully-populated {@link Recipient} based off of a {@link SignalServiceAddress},
|
||||
* creating one in the database if necessary. Convenience overload of
|
||||
* {@link #externalPush(Context, UUID, String)}
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull Recipient externalPush(@NonNull Context context, @NonNull SignalServiceAddress signalServiceAddress) {
|
||||
return externalPush(context, signalServiceAddress.getUuid().orNull(), signalServiceAddress.getNumber().orNull());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fully-populated {@link Recipient} based off of a UUID and phone number, creating one
|
||||
* in the database if necessary. We want both piece of information so we're able to associate them
|
||||
* both together, depending on which are available.
|
||||
*
|
||||
* In particular, while we'll eventually get the UUID of a user created via a phone number
|
||||
* (through a directory sync), the only way we can store the phone number is by retrieving it from
|
||||
* sent messages and whatnot. So we should store it when available.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull Recipient externalPush(@NonNull Context context, @Nullable UUID uuid, @Nullable String e164) {
|
||||
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(context);
|
||||
Optional<RecipientId> uuidUser = uuid != null ? db.getByUuid(uuid) : Optional.absent();
|
||||
Optional<RecipientId> e164User = e164 != null ? db.getByE164(e164) : Optional.absent();
|
||||
|
||||
if (uuidUser.isPresent()) {
|
||||
Recipient recipient = resolved(uuidUser.get());
|
||||
|
||||
if (e164 != null && !recipient.getE164().isPresent() && !e164User.isPresent()) {
|
||||
db.setPhoneNumber(recipient.getId(), e164);
|
||||
}
|
||||
|
||||
return resolved(recipient.getId());
|
||||
} else if (e164User.isPresent()) {
|
||||
Recipient recipient = resolved(e164User.get());
|
||||
|
||||
if (uuid != null && !recipient.getUuid().isPresent()) {
|
||||
db.markRegistered(recipient.getId(), uuid);
|
||||
} else if (!recipient.isRegistered()) {
|
||||
db.markRegistered(recipient.getId());
|
||||
|
||||
if (FeatureFlags.UUIDS) {
|
||||
Log.i(TAG, "No UUID! Scheduling a fetch.");
|
||||
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(recipient, false));
|
||||
}
|
||||
}
|
||||
|
||||
return resolved(recipient.getId());
|
||||
} else if (uuid != null) {
|
||||
if (FeatureFlags.UUIDS || e164 != null) {
|
||||
RecipientId id = db.getOrInsertFromUuid(uuid);
|
||||
db.markRegistered(id, uuid);
|
||||
|
||||
if (e164 != null) {
|
||||
db.setPhoneNumber(id, e164);
|
||||
}
|
||||
|
||||
return resolved(id);
|
||||
} else {
|
||||
throw new UuidRecipientError();
|
||||
}
|
||||
} else if (e164 != null) {
|
||||
Recipient recipient = resolved(db.getOrInsertFromE164(e164));
|
||||
|
||||
if (!recipient.isRegistered()) {
|
||||
db.markRegistered(recipient.getId());
|
||||
|
||||
if (FeatureFlags.UUIDS) {
|
||||
Log.i(TAG, "No UUID! Scheduling a fetch.");
|
||||
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(recipient, false));
|
||||
}
|
||||
}
|
||||
|
||||
return resolved(recipient.getId());
|
||||
} else {
|
||||
throw new AssertionError("You must provide either a UUID or phone number!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A safety wrapper around {@link #external(Context, String)} for when you know you're using an
|
||||
* identifier for a system contact, and therefore always want to prevent interpreting it as a
|
||||
* UUID. This will crash if given a UUID.
|
||||
*
|
||||
* (This may seem strange, but apparently some devices are returning valid UUIDs for contacts)
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull Recipient externalContact(@NonNull Context context, @NonNull String identifier) {
|
||||
if (UuidUtil.isUuid(identifier)) {
|
||||
throw new UuidRecipientError();
|
||||
} else {
|
||||
return external(context, identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fully-populated {@link Recipient} based off of a string identifier, creating one in
|
||||
* the database if necessary. The identifier may be a uuid, phone number, email,
|
||||
* or serialized groupId.
|
||||
*
|
||||
* If the identifier is a UUID of a Signal user, prefer using
|
||||
* {@link #externalPush(Context, UUID, String)} or its overload, as this will let us associate
|
||||
* the phone number with the recipient.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull Recipient external(@NonNull Context context, @NonNull String identifier) {
|
||||
Preconditions.checkNotNull(identifier, "Identifier cannot be null!");
|
||||
|
||||
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(context);
|
||||
RecipientId id = null;
|
||||
|
||||
if (UuidUtil.isUuid(identifier)) {
|
||||
UUID uuid = UuidUtil.parseOrThrow(identifier);
|
||||
|
||||
if (FeatureFlags.UUIDS) {
|
||||
id = db.getOrInsertFromUuid(uuid);
|
||||
} else {
|
||||
Optional<RecipientId> possibleId = db.getByUuid(uuid);
|
||||
|
||||
if (possibleId.isPresent()) {
|
||||
id = possibleId.get();
|
||||
} else {
|
||||
throw new UuidRecipientError();
|
||||
}
|
||||
}
|
||||
} else if (GroupUtil.isEncodedGroup(identifier)) {
|
||||
id = db.getOrInsertFromGroupId(identifier);
|
||||
} else if (NumberUtil.isValidEmail(identifier)) {
|
||||
id = db.getOrInsertFromEmail(identifier);
|
||||
} else {
|
||||
String e164 = PhoneNumberFormatter.get(context).format(identifier);
|
||||
id = db.getOrInsertFromE164(e164);
|
||||
}
|
||||
|
||||
return Recipient.resolved(id);
|
||||
}
|
||||
|
||||
public static @NonNull Recipient self() {
|
||||
return ApplicationDependencies.getRecipientCache().getSelf();
|
||||
}
|
||||
|
||||
Recipient(@NonNull RecipientId id) {
|
||||
this.id = id;
|
||||
this.resolving = true;
|
||||
this.uuid = null;
|
||||
this.username = null;
|
||||
this.e164 = null;
|
||||
this.email = null;
|
||||
this.groupId = null;
|
||||
this.participants = Collections.emptyList();
|
||||
this.groupAvatarId = Optional.absent();
|
||||
this.localNumber = false;
|
||||
this.blocked = false;
|
||||
this.muteUntil = 0;
|
||||
this.messageVibrate = VibrateState.DEFAULT;
|
||||
this.callVibrate = VibrateState.DEFAULT;
|
||||
this.messageRingtone = null;
|
||||
this.callRingtone = null;
|
||||
this.color = null;
|
||||
this.insightsBannerTier = InsightsBannerTier.TIER_TWO;
|
||||
this.defaultSubscriptionId = Optional.absent();
|
||||
this.expireMessages = 0;
|
||||
this.registered = RegisteredState.UNKNOWN;
|
||||
this.profileKey = null;
|
||||
this.name = null;
|
||||
this.systemContactPhoto = null;
|
||||
this.customLabel = null;
|
||||
this.contactUri = null;
|
||||
this.profileName = null;
|
||||
this.profileAvatar = null;
|
||||
this.profileSharing = false;
|
||||
this.notificationChannel = null;
|
||||
this.unidentifiedAccessMode = UnidentifiedAccessMode.DISABLED;
|
||||
this.forceSmsSelection = false;
|
||||
this.uuidSupported = false;
|
||||
this.storageKey = null;
|
||||
this.identityKey = null;
|
||||
this.identityStatus = VerifiedStatus.DEFAULT;
|
||||
}
|
||||
|
||||
Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details) {
|
||||
this.id = id;
|
||||
this.resolving = false;
|
||||
this.uuid = details.uuid;
|
||||
this.username = details.username;
|
||||
this.e164 = details.e164;
|
||||
this.email = details.email;
|
||||
this.groupId = details.groupId;
|
||||
this.participants = details.participants;
|
||||
this.groupAvatarId = details.groupAvatarId;
|
||||
this.localNumber = details.isLocalNumber;
|
||||
this.blocked = details.blocked;
|
||||
this.muteUntil = details.mutedUntil;
|
||||
this.messageVibrate = details.messageVibrateState;
|
||||
this.callVibrate = details.callVibrateState;
|
||||
this.messageRingtone = details.messageRingtone;
|
||||
this.callRingtone = details.callRingtone;
|
||||
this.color = details.color;
|
||||
this.insightsBannerTier = details.insightsBannerTier;
|
||||
this.defaultSubscriptionId = details.defaultSubscriptionId;
|
||||
this.expireMessages = details.expireMessages;
|
||||
this.registered = details.registered;
|
||||
this.profileKey = details.profileKey;
|
||||
this.name = details.name;
|
||||
this.systemContactPhoto = details.systemContactPhoto;
|
||||
this.customLabel = details.customLabel;
|
||||
this.contactUri = details.contactUri;
|
||||
this.profileName = details.profileName;
|
||||
this.profileAvatar = details.profileAvatar;
|
||||
this.profileSharing = details.profileSharing;
|
||||
this.notificationChannel = details.notificationChannel;
|
||||
this.unidentifiedAccessMode = details.unidentifiedAccessMode;
|
||||
this.forceSmsSelection = details.forceSmsSelection;
|
||||
this.uuidSupported = details.uuidSuported;
|
||||
this.storageKey = details.storageKey;
|
||||
this.identityKey = details.identityKey;
|
||||
this.identityStatus = details.identityStatus;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isLocalNumber() {
|
||||
return localNumber;
|
||||
}
|
||||
|
||||
public @Nullable Uri getContactUri() {
|
||||
return contactUri;
|
||||
}
|
||||
|
||||
public @Nullable String getName(@NonNull Context context) {
|
||||
if (this.name == null && groupId != null && GroupUtil.isMmsGroup(groupId)) {
|
||||
List<String> names = new LinkedList<>();
|
||||
|
||||
for (Recipient recipient : participants) {
|
||||
names.add(recipient.toShortString(context));
|
||||
}
|
||||
|
||||
return Util.join(names, ", ");
|
||||
}
|
||||
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO [UUID] -- Remove once UUID Feature Flag is removed
|
||||
*/
|
||||
@Deprecated
|
||||
public @NonNull String toShortString(@NonNull Context context) {
|
||||
if (FeatureFlags.PROFILE_DISPLAY) return getDisplayName(context);
|
||||
else return Optional.fromNullable(getName(context)).or(getSmsAddress()).or("");
|
||||
}
|
||||
|
||||
public @NonNull String getDisplayName(@NonNull Context context) {
|
||||
return Util.getFirstNonEmpty(getName(context),
|
||||
getProfileName(),
|
||||
getDisplayUsername(),
|
||||
e164,
|
||||
email,
|
||||
context.getString(R.string.Recipient_unknown));
|
||||
}
|
||||
|
||||
public @NonNull MaterialColor getColor() {
|
||||
if (isGroupInternal()) return MaterialColor.GROUP;
|
||||
else if (color != null) return color;
|
||||
else if (name != null) return ContactColors.generateFor(name);
|
||||
else return ContactColors.UNKNOWN_COLOR;
|
||||
}
|
||||
|
||||
public @NonNull Optional<UUID> getUuid() {
|
||||
return Optional.fromNullable(uuid);
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getUsername() {
|
||||
if (FeatureFlags.USERNAMES) {
|
||||
return Optional.fromNullable(username);
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getE164() {
|
||||
return Optional.fromNullable(e164);
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getEmail() {
|
||||
return Optional.fromNullable(email);
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getGroupId() {
|
||||
return Optional.fromNullable(groupId);
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getSmsAddress() {
|
||||
return Optional.fromNullable(e164).or(Optional.fromNullable(email));
|
||||
}
|
||||
|
||||
public @NonNull String requireE164() {
|
||||
String resolved = resolving ? resolve().e164 : e164;
|
||||
|
||||
if (resolved == null) {
|
||||
throw new MissingAddressError();
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public @NonNull String requireEmail() {
|
||||
String resolved = resolving ? resolve().email : email;
|
||||
|
||||
if (resolved == null) {
|
||||
throw new MissingAddressError();
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public @NonNull String requireSmsAddress() {
|
||||
Recipient recipient = resolving ? resolve() : this;
|
||||
|
||||
if (recipient.getE164().isPresent()) {
|
||||
return recipient.getE164().get();
|
||||
} else if (recipient.getEmail().isPresent()) {
|
||||
return recipient.getEmail().get();
|
||||
} else {
|
||||
throw new MissingAddressError();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSmsAddress() {
|
||||
return getE164().or(getEmail()).isPresent();
|
||||
}
|
||||
|
||||
public boolean hasE164() {
|
||||
return getE164().isPresent();
|
||||
}
|
||||
|
||||
public boolean hasUuid() {
|
||||
return getUuid().isPresent();
|
||||
}
|
||||
|
||||
public @NonNull String requireGroupId() {
|
||||
String resolved = resolving ? resolve().groupId : groupId;
|
||||
|
||||
if (resolved == null) {
|
||||
throw new MissingAddressError();
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public boolean hasServiceIdentifier() {
|
||||
return uuid != null || e164 != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A string identifier able to be used with the Signal service. Prefers UUID, and if not
|
||||
* available, will return an E164 number.
|
||||
*/
|
||||
public @NonNull String requireServiceId() {
|
||||
Recipient resolved = resolving ? resolve() : this;
|
||||
|
||||
if (resolved.getUuid().isPresent()) {
|
||||
return resolved.getUuid().get().toString();
|
||||
} else {
|
||||
return getE164().get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A single string to represent the recipient, in order of precedence:
|
||||
*
|
||||
* Group ID > UUID > Phone > Email
|
||||
*/
|
||||
public @NonNull String requireStringId() {
|
||||
Recipient resolved = resolving ? resolve() : this;
|
||||
|
||||
if (resolved.isGroup()) {
|
||||
return resolved.requireGroupId();
|
||||
} else if (resolved.getUuid().isPresent()) {
|
||||
return resolved.getUuid().get().toString();
|
||||
}
|
||||
|
||||
return requireSmsAddress();
|
||||
}
|
||||
|
||||
public Optional<Integer> getDefaultSubscriptionId() {
|
||||
return defaultSubscriptionId;
|
||||
}
|
||||
|
||||
public @Nullable String getProfileName() {
|
||||
return profileName;
|
||||
}
|
||||
|
||||
public @Nullable String getCustomLabel() {
|
||||
if (FeatureFlags.PROFILE_DISPLAY) throw new AssertionError("This method should never be called if PROFILE_DISPLAY is enabled.");
|
||||
return customLabel;
|
||||
}
|
||||
|
||||
public @Nullable String getProfileAvatar() {
|
||||
return profileAvatar;
|
||||
}
|
||||
|
||||
public boolean isProfileSharing() {
|
||||
return profileSharing;
|
||||
}
|
||||
|
||||
public boolean isGroup() {
|
||||
return resolve().groupId != null;
|
||||
}
|
||||
|
||||
private boolean isGroupInternal() {
|
||||
return groupId != null;
|
||||
}
|
||||
|
||||
public boolean isMmsGroup() {
|
||||
String groupId = resolve().groupId;
|
||||
return groupId != null && GroupUtil.isMmsGroup(groupId);
|
||||
}
|
||||
|
||||
public boolean isPushGroup() {
|
||||
String groupId = resolve().groupId;
|
||||
return groupId != null && !GroupUtil.isMmsGroup(groupId);
|
||||
}
|
||||
|
||||
public @NonNull List<Recipient> getParticipants() {
|
||||
return new ArrayList<>(participants);
|
||||
}
|
||||
|
||||
public @NonNull Drawable getFallbackContactPhotoDrawable(Context context, boolean inverted) {
|
||||
return getFallbackContactPhoto().asDrawable(context, getColor().toAvatarColor(context), inverted);
|
||||
}
|
||||
|
||||
public @NonNull Drawable getSmallFallbackContactPhotoDrawable(Context context, boolean inverted) {
|
||||
return getFallbackContactPhoto().asSmallDrawable(context, getColor().toAvatarColor(context), inverted);
|
||||
}
|
||||
|
||||
public @NonNull FallbackContactPhoto getFallbackContactPhoto() {
|
||||
if (localNumber) return new ResourceContactPhoto(R.drawable.ic_note_to_self);
|
||||
if (isResolving()) return new TransparentContactPhoto();
|
||||
else if (isGroupInternal()) return new ResourceContactPhoto(R.drawable.ic_group_outline_40, R.drawable.ic_group_outline_20, R.drawable.ic_group_large);
|
||||
else if (isGroup()) return new ResourceContactPhoto(R.drawable.ic_group_outline_40, R.drawable.ic_group_outline_20, R.drawable.ic_group_large);
|
||||
else if (!TextUtils.isEmpty(name)) return new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40);
|
||||
else return new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large);
|
||||
}
|
||||
|
||||
public @Nullable ContactPhoto getContactPhoto() {
|
||||
if (localNumber) return null;
|
||||
else if (isGroupInternal() && groupAvatarId.isPresent()) return new GroupRecordContactPhoto(groupId, groupAvatarId.get());
|
||||
else if (systemContactPhoto != null) return new SystemContactPhoto(id, systemContactPhoto, 0);
|
||||
else if (profileAvatar != null) return new ProfileContactPhoto(id, profileAvatar);
|
||||
else return null;
|
||||
}
|
||||
|
||||
public @Nullable Uri getMessageRingtone() {
|
||||
if (messageRingtone != null && messageRingtone.getScheme() != null && messageRingtone.getScheme().startsWith("file")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return messageRingtone;
|
||||
}
|
||||
|
||||
public @Nullable Uri getCallRingtone() {
|
||||
if (callRingtone != null && callRingtone.getScheme() != null && callRingtone.getScheme().startsWith("file")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return callRingtone;
|
||||
}
|
||||
|
||||
public boolean isMuted() {
|
||||
return System.currentTimeMillis() <= muteUntil;
|
||||
}
|
||||
|
||||
public boolean isBlocked() {
|
||||
return blocked;
|
||||
}
|
||||
|
||||
public @NonNull VibrateState getMessageVibrate() {
|
||||
return messageVibrate;
|
||||
}
|
||||
|
||||
public @NonNull VibrateState getCallVibrate() {
|
||||
return callVibrate;
|
||||
}
|
||||
|
||||
public int getExpireMessages() {
|
||||
return expireMessages;
|
||||
}
|
||||
|
||||
public boolean hasSeenFirstInviteReminder() {
|
||||
return insightsBannerTier.seen(InsightsBannerTier.TIER_ONE);
|
||||
}
|
||||
|
||||
public boolean hasSeenSecondInviteReminder() {
|
||||
return insightsBannerTier.seen(InsightsBannerTier.TIER_TWO);
|
||||
}
|
||||
|
||||
public @NonNull RegisteredState getRegistered() {
|
||||
if (isPushGroup()) return RegisteredState.REGISTERED;
|
||||
else if (isMmsGroup()) return RegisteredState.NOT_REGISTERED;
|
||||
|
||||
return registered;
|
||||
}
|
||||
|
||||
public boolean isRegistered() {
|
||||
return registered == RegisteredState.REGISTERED || isPushGroup();
|
||||
}
|
||||
|
||||
public @Nullable String getNotificationChannel() {
|
||||
return !NotificationChannels.supported() ? null : notificationChannel;
|
||||
}
|
||||
|
||||
public boolean isForceSmsSelection() {
|
||||
return forceSmsSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if this recipient can support receiving UUID-only messages, otherwise false.
|
||||
*/
|
||||
public boolean isUuidSupported() {
|
||||
if (FeatureFlags.USERNAMES) {
|
||||
return true;
|
||||
} else {
|
||||
return FeatureFlags.UUIDS && uuidSupported;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable byte[] getProfileKey() {
|
||||
return profileKey;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getStorageServiceKey() {
|
||||
return storageKey;
|
||||
}
|
||||
|
||||
public @NonNull VerifiedStatus getIdentityVerifiedStatus() {
|
||||
return identityStatus;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public @NonNull UnidentifiedAccessMode getUnidentifiedAccessMode() {
|
||||
return unidentifiedAccessMode;
|
||||
}
|
||||
|
||||
public boolean isSystemContact() {
|
||||
return contactUri != null;
|
||||
}
|
||||
|
||||
public Recipient resolve() {
|
||||
if (resolving) {
|
||||
return live().resolve();
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isResolving() {
|
||||
return resolving;
|
||||
}
|
||||
|
||||
public @NonNull LiveRecipient live() {
|
||||
return ApplicationDependencies.getRecipientCache().getLive(id);
|
||||
}
|
||||
|
||||
private @Nullable String getDisplayUsername() {
|
||||
if (!TextUtils.isEmpty(username)) {
|
||||
return "@" + username;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Recipient recipient = (Recipient) o;
|
||||
return id.equals(recipient.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
private static class MissingAddressError extends AssertionError {
|
||||
}
|
||||
|
||||
private static class UuidRecipientError extends AssertionError {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
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.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class RecipientDetails {
|
||||
|
||||
final UUID uuid;
|
||||
final String username;
|
||||
final String e164;
|
||||
final String email;
|
||||
final String groupId;
|
||||
final String name;
|
||||
final String customLabel;
|
||||
final Uri systemContactPhoto;
|
||||
final Uri contactUri;
|
||||
final Optional<Long> groupAvatarId;
|
||||
final MaterialColor color;
|
||||
final Uri messageRingtone;
|
||||
final Uri callRingtone;
|
||||
final long mutedUntil;
|
||||
final VibrateState messageVibrateState;
|
||||
final VibrateState callVibrateState;
|
||||
final boolean blocked;
|
||||
final int expireMessages;
|
||||
final List<Recipient> participants;
|
||||
final String profileName;
|
||||
final Optional<Integer> defaultSubscriptionId;
|
||||
final RegisteredState registered;
|
||||
final byte[] profileKey;
|
||||
final String profileAvatar;
|
||||
final boolean profileSharing;
|
||||
final boolean systemContact;
|
||||
final boolean isLocalNumber;
|
||||
final String notificationChannel;
|
||||
final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
final boolean forceSmsSelection;
|
||||
final boolean uuidSuported;
|
||||
final InsightsBannerTier insightsBannerTier;
|
||||
final byte[] storageKey;
|
||||
final byte[] identityKey;
|
||||
final VerifiedStatus identityStatus;
|
||||
|
||||
RecipientDetails(@NonNull Context context,
|
||||
@Nullable String name,
|
||||
@NonNull Optional<Long> groupAvatarId,
|
||||
boolean systemContact,
|
||||
boolean isLocalNumber,
|
||||
@NonNull RecipientSettings settings,
|
||||
@Nullable List<Recipient> participants)
|
||||
{
|
||||
this.groupAvatarId = groupAvatarId;
|
||||
this.systemContactPhoto = Util.uri(settings.getSystemContactPhotoUri());
|
||||
this.customLabel = settings.getSystemPhoneLabel();
|
||||
this.contactUri = Util.uri(settings.getSystemContactUri());
|
||||
this.uuid = settings.getUuid();
|
||||
this.username = settings.getUsername();
|
||||
this.e164 = settings.getE164();
|
||||
this.email = settings.getEmail();
|
||||
this.groupId = settings.getGroupId();
|
||||
this.color = settings.getColor();
|
||||
this.messageRingtone = settings.getMessageRingtone();
|
||||
this.callRingtone = settings.getCallRingtone();
|
||||
this.mutedUntil = settings.getMuteUntil();
|
||||
this.messageVibrateState = settings.getMessageVibrateState();
|
||||
this.callVibrateState = settings.getCallVibrateState();
|
||||
this.blocked = settings.isBlocked();
|
||||
this.expireMessages = settings.getExpireMessages();
|
||||
this.participants = participants == null ? new LinkedList<>() : participants;
|
||||
this.profileName = isLocalNumber ? TextSecurePreferences.getProfileName(context) : settings.getProfileName();
|
||||
this.defaultSubscriptionId = settings.getDefaultSubscriptionId();
|
||||
this.registered = settings.getRegistered();
|
||||
this.profileKey = isLocalNumber ? ProfileKeyUtil.getProfileKey(context) : settings.getProfileKey();
|
||||
this.profileAvatar = settings.getProfileAvatar();
|
||||
this.profileSharing = settings.isProfileSharing();
|
||||
this.systemContact = systemContact;
|
||||
this.isLocalNumber = isLocalNumber;
|
||||
this.notificationChannel = settings.getNotificationChannel();
|
||||
this.unidentifiedAccessMode = settings.getUnidentifiedAccessMode();
|
||||
this.forceSmsSelection = settings.isForceSmsSelection();
|
||||
this.uuidSuported = settings.isUuidSupported();
|
||||
this.insightsBannerTier = settings.getInsightsBannerTier();
|
||||
this.storageKey = settings.getStorageKey();
|
||||
this.identityKey = settings.getIdentityKey();
|
||||
this.identityStatus = settings.getIdentityStatus();
|
||||
|
||||
if (name == null) this.name = settings.getSystemDisplayName();
|
||||
else this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only used for {@link Recipient#UNKNOWN}.
|
||||
*/
|
||||
RecipientDetails() {
|
||||
this.groupAvatarId = null;
|
||||
this.systemContactPhoto = null;
|
||||
this.customLabel = null;
|
||||
this.contactUri = null;
|
||||
this.uuid = null;
|
||||
this.username = null;
|
||||
this.e164 = null;
|
||||
this.email = null;
|
||||
this.groupId = null;
|
||||
this.color = null;
|
||||
this.messageRingtone = null;
|
||||
this.callRingtone = null;
|
||||
this.mutedUntil = 0;
|
||||
this.messageVibrateState = VibrateState.DEFAULT;
|
||||
this.callVibrateState = VibrateState.DEFAULT;
|
||||
this.blocked = false;
|
||||
this.expireMessages = 0;
|
||||
this.participants = new LinkedList<>();
|
||||
this.profileName = null;
|
||||
this.insightsBannerTier = InsightsBannerTier.TIER_TWO;
|
||||
this.defaultSubscriptionId = Optional.absent();
|
||||
this.registered = RegisteredState.UNKNOWN;
|
||||
this.profileKey = null;
|
||||
this.profileAvatar = null;
|
||||
this.profileSharing = false;
|
||||
this.systemContact = true;
|
||||
this.isLocalNumber = false;
|
||||
this.notificationChannel = null;
|
||||
this.unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
|
||||
this.forceSmsSelection = false;
|
||||
this.name = null;
|
||||
this.uuidSuported = false;
|
||||
this.storageKey = null;
|
||||
this.identityKey = null;
|
||||
this.identityStatus = VerifiedStatus.DEFAULT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.provider.ContactsContract;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import static android.content.Intent.ACTION_INSERT_OR_EDIT;
|
||||
|
||||
public final class RecipientExporter {
|
||||
|
||||
public static RecipientExporter export(Recipient recipient) {
|
||||
return new RecipientExporter(recipient);
|
||||
}
|
||||
|
||||
private final Recipient recipient;
|
||||
|
||||
private RecipientExporter(Recipient recipient) {
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
public Intent asAddContactIntent() {
|
||||
Intent intent = new Intent(ACTION_INSERT_OR_EDIT);
|
||||
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
|
||||
addNameToIntent(intent, recipient.getProfileName());
|
||||
addAddressToIntent(intent, recipient);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private static void addNameToIntent(Intent intent, String profileName) {
|
||||
if (!TextUtils.isEmpty(profileName)) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.NAME, profileName);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addAddressToIntent(Intent intent, Recipient recipient) {
|
||||
if (recipient.getE164().isPresent()) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.PHONE, recipient.requireE164());
|
||||
} else if (recipient.getEmail().isPresent()) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.EMAIL, recipient.requireEmail());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface RecipientForeverObserver {
|
||||
@MainThread
|
||||
void onRecipientChanged(@NonNull Recipient recipient);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
public class RecipientFormattingException extends Exception {
|
||||
public RecipientFormattingException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public RecipientFormattingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public RecipientFormattingException(String message, Throwable nested) {
|
||||
super(message, nested);
|
||||
}
|
||||
|
||||
public RecipientFormattingException(Throwable nested) {
|
||||
super(nested);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.util.DelimiterUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class RecipientId implements Parcelable, Comparable<RecipientId> {
|
||||
|
||||
private static final long UNKNOWN_ID = -1;
|
||||
private static final char DELIMITER = ',';
|
||||
|
||||
public static final RecipientId UNKNOWN = RecipientId.from(UNKNOWN_ID);
|
||||
|
||||
private final long id;
|
||||
|
||||
public static RecipientId from(long id) {
|
||||
if (id == 0) {
|
||||
throw new InvalidLongRecipientIdError();
|
||||
}
|
||||
|
||||
return new RecipientId(id);
|
||||
}
|
||||
|
||||
public static RecipientId from(@NonNull String id) {
|
||||
try {
|
||||
return RecipientId.from(Long.parseLong(id));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new InvalidStringRecipientIdError();
|
||||
}
|
||||
}
|
||||
|
||||
private RecipientId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
private RecipientId(Parcel in) {
|
||||
id = in.readLong();
|
||||
}
|
||||
|
||||
public static @NonNull String toSerializedList(@NonNull List<RecipientId> ids) {
|
||||
return Util.join(Stream.of(ids).map(RecipientId::serialize).toList(), String.valueOf(DELIMITER));
|
||||
}
|
||||
|
||||
public static List<RecipientId> fromSerializedList(@NonNull String serialized) {
|
||||
String[] stringIds = DelimiterUtil.split(serialized, DELIMITER);
|
||||
List<RecipientId> out = new ArrayList<>(stringIds.length);
|
||||
|
||||
for (String stringId : stringIds) {
|
||||
RecipientId id = RecipientId.from(Long.parseLong(stringId));
|
||||
out.add(id);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
public boolean isUnknown() {
|
||||
return id == UNKNOWN_ID;
|
||||
}
|
||||
|
||||
public @NonNull String serialize() {
|
||||
return String.valueOf(id);
|
||||
}
|
||||
|
||||
public long toLong() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public @NonNull String toQueueKey() {
|
||||
return "RecipientId::" + id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return "RecipientId::" + id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
RecipientId that = (RecipientId) o;
|
||||
|
||||
return id == that.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (int) (id ^ (id >>> 32));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(RecipientId o) {
|
||||
return Long.compare(this.id, o.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeLong(id);
|
||||
}
|
||||
|
||||
public static final Creator<RecipientId> CREATOR = new Creator<RecipientId>() {
|
||||
@Override
|
||||
public RecipientId createFromParcel(Parcel in) {
|
||||
return new RecipientId(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipientId[] newArray(int size) {
|
||||
return new RecipientId[size];
|
||||
}
|
||||
};
|
||||
|
||||
private static class InvalidLongRecipientIdError extends AssertionError {}
|
||||
private static class InvalidStringRecipientIdError extends AssertionError {}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface RecipientModifiedListener {
|
||||
public void onModified(@NonNull Recipient recipient);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class RecipientUtil {
|
||||
|
||||
private static final String TAG = Log.tag(RecipientUtil.class);
|
||||
|
||||
/**
|
||||
* This method will do it's best to craft a fully-populated {@link SignalServiceAddress} based on
|
||||
* the provided recipient. This includes performing a possible network request if no UUID is
|
||||
* available.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull SignalServiceAddress toSignalServiceAddress(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
recipient = recipient.resolve();
|
||||
|
||||
if (!recipient.getUuid().isPresent() && !recipient.getE164().isPresent()) {
|
||||
throw new AssertionError(recipient.getId() + " - No UUID or phone number!");
|
||||
}
|
||||
|
||||
if (FeatureFlags.UUIDS && !recipient.getUuid().isPresent()) {
|
||||
Log.i(TAG, recipient.getId() + " is missing a UUID...");
|
||||
try {
|
||||
RegisteredState state = DirectoryHelper.refreshDirectoryFor(context, recipient, false);
|
||||
recipient = Recipient.resolved(recipient.getId());
|
||||
Log.i(TAG, "Successfully performed a UUID fetch for " + recipient.getId() + ". Registered: " + state);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to fetch a UUID for " + recipient.getId() + ". Scheduling a future fetch and building an address without one.");
|
||||
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(recipient, false));
|
||||
}
|
||||
}
|
||||
|
||||
return new SignalServiceAddress(Optional.fromNullable(recipient.getUuid().orNull()), Optional.fromNullable(recipient.resolve().getE164().orNull()));
|
||||
}
|
||||
|
||||
public static boolean isBlockable(@NonNull Recipient recipient) {
|
||||
Recipient resolved = recipient.resolve();
|
||||
return resolved.isPushGroup() || resolved.hasServiceIdentifier();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void block(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
if (!isBlockable(recipient)) {
|
||||
throw new AssertionError("Recipient is not blockable!");
|
||||
}
|
||||
|
||||
Recipient resolved = recipient.resolve();
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).setBlocked(resolved.getId(), true);
|
||||
|
||||
if (resolved.isGroup() && DatabaseFactory.getGroupDatabase(context).isActive(resolved.requireGroupId())) {
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(resolved);
|
||||
Optional<OutgoingGroupMediaMessage> leaveMessage = GroupUtil.createGroupLeaveMessage(context, resolved);
|
||||
|
||||
if (threadId != -1 && leaveMessage.isPresent()) {
|
||||
MessageSender.send(context, leaveMessage.get(), threadId, false, null);
|
||||
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
String groupId = resolved.requireGroupId();
|
||||
groupDatabase.setActive(groupId, false);
|
||||
groupDatabase.remove(groupId, Recipient.self().getId());
|
||||
} else {
|
||||
Log.w(TAG, "Failed to leave group. Can't block.");
|
||||
Toast.makeText(context, R.string.RecipientPreferenceActivity_error_leaving_group, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
if (resolved.isSystemContact() || resolved.isProfileSharing()) {
|
||||
ApplicationDependencies.getJobManager().add(new RotateProfileKeyJob());
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void unblock(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
if (!isBlockable(recipient)) {
|
||||
throw new AssertionError("Recipient is not blockable!");
|
||||
}
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).setBlocked(recipient.getId(), false);
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static boolean isRecipientMessageRequestAccepted(@NonNull Context context, @Nullable Recipient recipient) {
|
||||
if (recipient == null || !FeatureFlags.MESSAGE_REQUESTS) return true;
|
||||
|
||||
Recipient resolved = recipient.resolve();
|
||||
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
long threadId = threadDatabase.getThreadIdFor(resolved);
|
||||
boolean hasSentMessage = threadDatabase.getLastSeenAndHasSent(threadId).second() == Boolean.TRUE;
|
||||
|
||||
return hasSentMessage || resolved.isProfileSharing() || resolved.isSystemContact();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.recipients;
|
||||
|
||||
import android.telephony.PhoneNumberUtils;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
public class RecipientsFormatter {
|
||||
|
||||
private static String parseBracketedNumber(String recipient) throws RecipientFormattingException {
|
||||
int begin = recipient.indexOf('<');
|
||||
int end = recipient.indexOf('>');
|
||||
String value = recipient.substring(begin + 1, end);
|
||||
|
||||
if (PhoneNumberUtils.isWellFormedSmsAddress(value))
|
||||
return value;
|
||||
else
|
||||
throw new RecipientFormattingException("Bracketed value: " + value + " is not valid.");
|
||||
}
|
||||
|
||||
private static String parseRecipient(String recipient) throws RecipientFormattingException {
|
||||
recipient = recipient.trim();
|
||||
|
||||
if ((recipient.indexOf('<') != -1) && (recipient.indexOf('>') != -1))
|
||||
return parseBracketedNumber(recipient);
|
||||
|
||||
if (PhoneNumberUtils.isWellFormedSmsAddress(recipient))
|
||||
return recipient;
|
||||
|
||||
throw new RecipientFormattingException("Recipient: " + recipient + " is badly formatted.");
|
||||
}
|
||||
|
||||
public static List<String> getRecipients(String rawText) throws RecipientFormattingException {
|
||||
ArrayList<String> results = new ArrayList<String>();
|
||||
StringTokenizer tokenizer = new StringTokenizer(rawText, ",");
|
||||
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
results.add(parseRecipient(tokenizer.nextToken()));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public static String formatNameAndNumber(String name, String number) {
|
||||
// Format like this: Mike Cleron <(650) 555-1234>
|
||||
// Erick Tseng <(650) 555-1212>
|
||||
// Tutankhamun <tutank1341@gmail.com>
|
||||
// (408) 555-1289
|
||||
String formattedNumber = PhoneNumberUtils.formatNumber(number);
|
||||
if (!TextUtils.isEmpty(name) && !name.equals(number)) {
|
||||
return name + " <" + formattedNumber + ">";
|
||||
} else {
|
||||
return formattedNumber;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user