Move all files to natural position.

This commit is contained in:
Alan Evans
2020-01-06 10:52:48 -05:00
parent 0df36047e7
commit 9ebe920195
3016 changed files with 6 additions and 36 deletions

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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 {
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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 {}
}

View File

@@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.recipients;
import androidx.annotation.NonNull;
public interface RecipientModifiedListener {
public void onModified(@NonNull Recipient recipient);
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}