mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-07 07:24:06 +00:00
restructure and unite service android/java to libsignal
This commit is contained in:
@@ -0,0 +1,666 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class Contact implements Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final Name name;
|
||||
|
||||
@JsonProperty
|
||||
private final String organization;
|
||||
|
||||
@JsonProperty
|
||||
private final List<Phone> phoneNumbers;
|
||||
|
||||
@JsonProperty
|
||||
private final List<Email> emails;
|
||||
|
||||
@JsonProperty
|
||||
private final List<PostalAddress> postalAddresses;
|
||||
|
||||
@JsonProperty
|
||||
private final Avatar avatar;
|
||||
|
||||
public Contact(@JsonProperty("name") @NonNull Name name,
|
||||
@JsonProperty("organization") @Nullable String organization,
|
||||
@JsonProperty("phoneNumbers") @NonNull List<Phone> phoneNumbers,
|
||||
@JsonProperty("emails") @NonNull List<Email> emails,
|
||||
@JsonProperty("postalAddresses") @NonNull List<PostalAddress> postalAddresses,
|
||||
@JsonProperty("avatar") @Nullable Avatar avatar)
|
||||
{
|
||||
this.name = name;
|
||||
this.organization = organization;
|
||||
this.phoneNumbers = Collections.unmodifiableList(phoneNumbers);
|
||||
this.emails = Collections.unmodifiableList(emails);
|
||||
this.postalAddresses = Collections.unmodifiableList(postalAddresses);
|
||||
this.avatar = avatar;
|
||||
}
|
||||
|
||||
public Contact(@NonNull Contact contact, @Nullable Avatar avatar) {
|
||||
this(contact.getName(),
|
||||
contact.getOrganization(),
|
||||
contact.getPhoneNumbers(),
|
||||
contact.getEmails(),
|
||||
contact.getPostalAddresses(),
|
||||
avatar);
|
||||
}
|
||||
|
||||
private Contact(Parcel in) {
|
||||
this(in.readParcelable(Name.class.getClassLoader()),
|
||||
in.readString(),
|
||||
in.createTypedArrayList(Phone.CREATOR),
|
||||
in.createTypedArrayList(Email.CREATOR),
|
||||
in.createTypedArrayList(PostalAddress.CREATOR),
|
||||
in.readParcelable(Avatar.class.getClassLoader()));
|
||||
}
|
||||
|
||||
public @NonNull Name getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public @Nullable String getOrganization() {
|
||||
return organization;
|
||||
}
|
||||
|
||||
public @NonNull List<Phone> getPhoneNumbers() {
|
||||
return phoneNumbers;
|
||||
}
|
||||
|
||||
public @NonNull List<Email> getEmails() {
|
||||
return emails;
|
||||
}
|
||||
|
||||
public @NonNull List<PostalAddress> getPostalAddresses() {
|
||||
return postalAddresses;
|
||||
}
|
||||
|
||||
public @Nullable Avatar getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public @Nullable Attachment getAvatarAttachment() {
|
||||
return avatar != null ? avatar.getAttachment() : null;
|
||||
}
|
||||
|
||||
public String serialize() throws IOException {
|
||||
return JsonUtils.toJson(this);
|
||||
}
|
||||
|
||||
public static Contact deserialize(@NonNull String serialized) throws IOException {
|
||||
return JsonUtils.fromJson(serialized, Contact.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(name, flags);
|
||||
dest.writeString(organization);
|
||||
dest.writeTypedList(phoneNumbers);
|
||||
dest.writeTypedList(emails);
|
||||
dest.writeTypedList(postalAddresses);
|
||||
dest.writeParcelable(avatar, flags);
|
||||
}
|
||||
|
||||
public static final Creator<Contact> CREATOR = new Creator<Contact>() {
|
||||
@Override
|
||||
public Contact createFromParcel(Parcel in) {
|
||||
return new Contact(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Contact[] newArray(int size) {
|
||||
return new Contact[size];
|
||||
}
|
||||
};
|
||||
|
||||
public static class Name implements Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final String displayName;
|
||||
|
||||
@JsonProperty
|
||||
private final String givenName;
|
||||
|
||||
@JsonProperty
|
||||
private final String familyName;
|
||||
|
||||
@JsonProperty
|
||||
private final String prefix;
|
||||
|
||||
@JsonProperty
|
||||
private final String suffix;
|
||||
|
||||
@JsonProperty
|
||||
private final String middleName;
|
||||
|
||||
Name(@JsonProperty("displayName") @Nullable String displayName,
|
||||
@JsonProperty("givenName") @Nullable String givenName,
|
||||
@JsonProperty("familyName") @Nullable String familyName,
|
||||
@JsonProperty("prefix") @Nullable String prefix,
|
||||
@JsonProperty("suffix") @Nullable String suffix,
|
||||
@JsonProperty("middleName") @Nullable String middleName)
|
||||
{
|
||||
this.displayName = displayName;
|
||||
this.givenName = givenName;
|
||||
this.familyName = familyName;
|
||||
this.prefix = prefix;
|
||||
this.suffix = suffix;
|
||||
this.middleName = middleName;
|
||||
}
|
||||
|
||||
private Name(Parcel in) {
|
||||
this(in.readString(), in.readString(), in.readString(), in.readString(), in.readString(), in.readString());
|
||||
}
|
||||
|
||||
public @Nullable String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public @Nullable String getGivenName() {
|
||||
return givenName;
|
||||
}
|
||||
|
||||
public @Nullable String getFamilyName() {
|
||||
return familyName;
|
||||
}
|
||||
|
||||
public @Nullable String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public @Nullable String getSuffix() {
|
||||
return suffix;
|
||||
}
|
||||
|
||||
public @Nullable String getMiddleName() {
|
||||
return middleName;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return TextUtils.isEmpty(displayName) &&
|
||||
TextUtils.isEmpty(givenName) &&
|
||||
TextUtils.isEmpty(familyName) &&
|
||||
TextUtils.isEmpty(prefix) &&
|
||||
TextUtils.isEmpty(suffix) &&
|
||||
TextUtils.isEmpty(middleName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(displayName);
|
||||
dest.writeString(givenName);
|
||||
dest.writeString(familyName);
|
||||
dest.writeString(prefix);
|
||||
dest.writeString(suffix);
|
||||
dest.writeString(middleName);
|
||||
}
|
||||
|
||||
public static final Creator<Name> CREATOR = new Creator<Name>() {
|
||||
@Override
|
||||
public Name createFromParcel(Parcel in) {
|
||||
return new Name(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Name[] newArray(int size) {
|
||||
return new Name[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static class Phone implements Selectable, Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final String number;
|
||||
|
||||
@JsonProperty
|
||||
private final Type type;
|
||||
|
||||
@JsonProperty
|
||||
private final String label;
|
||||
|
||||
@JsonIgnore
|
||||
private boolean selected;
|
||||
|
||||
Phone(@JsonProperty("number") @NonNull String number,
|
||||
@JsonProperty("type") @NonNull Type type,
|
||||
@JsonProperty("label") @Nullable String label)
|
||||
{
|
||||
this.number = number;
|
||||
this.type = type;
|
||||
this.label = label;
|
||||
this.selected = true;
|
||||
}
|
||||
|
||||
private Phone(Parcel in) {
|
||||
this(in.readString(), Type.valueOf(in.readString()), in.readString());
|
||||
}
|
||||
|
||||
public @NonNull String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public @NonNull Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public @Nullable String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(number);
|
||||
dest.writeString(type.name());
|
||||
dest.writeString(label);
|
||||
}
|
||||
|
||||
public static final Creator<Phone> CREATOR = new Creator<Phone>() {
|
||||
@Override
|
||||
public Phone createFromParcel(Parcel in) {
|
||||
return new Phone(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Phone[] newArray(int size) {
|
||||
return new Phone[size];
|
||||
}
|
||||
};
|
||||
|
||||
public enum Type {
|
||||
HOME, MOBILE, WORK, CUSTOM
|
||||
}
|
||||
}
|
||||
|
||||
public static class Email implements Selectable, Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final String email;
|
||||
|
||||
@JsonProperty
|
||||
private final Type type;
|
||||
|
||||
@JsonProperty
|
||||
private final String label;
|
||||
|
||||
@JsonIgnore
|
||||
private boolean selected;
|
||||
|
||||
Email(@JsonProperty("email") @NonNull String email,
|
||||
@JsonProperty("type") @NonNull Type type,
|
||||
@JsonProperty("label") @Nullable String label)
|
||||
{
|
||||
this.email = email;
|
||||
this.type = type;
|
||||
this.label = label;
|
||||
this.selected = true;
|
||||
}
|
||||
|
||||
private Email(Parcel in) {
|
||||
this(in.readString(), Type.valueOf(in.readString()), in.readString());
|
||||
}
|
||||
|
||||
public @NonNull String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public @NonNull Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public @NonNull String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(email);
|
||||
dest.writeString(type.name());
|
||||
dest.writeString(label);
|
||||
}
|
||||
|
||||
public static final Creator<Email> CREATOR = new Creator<Email>() {
|
||||
@Override
|
||||
public Email createFromParcel(Parcel in) {
|
||||
return new Email(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Email[] newArray(int size) {
|
||||
return new Email[size];
|
||||
}
|
||||
};
|
||||
|
||||
public enum Type {
|
||||
HOME, MOBILE, WORK, CUSTOM
|
||||
}
|
||||
}
|
||||
|
||||
public static class PostalAddress implements Selectable, Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final Type type;
|
||||
|
||||
@JsonProperty
|
||||
private final String label;
|
||||
|
||||
@JsonProperty
|
||||
private final String street;
|
||||
|
||||
@JsonProperty
|
||||
private final String poBox;
|
||||
|
||||
@JsonProperty
|
||||
private final String neighborhood;
|
||||
|
||||
@JsonProperty
|
||||
private final String city;
|
||||
|
||||
@JsonProperty
|
||||
private final String region;
|
||||
|
||||
@JsonProperty
|
||||
private final String postalCode;
|
||||
|
||||
@JsonProperty
|
||||
private final String country;
|
||||
|
||||
@JsonIgnore
|
||||
private boolean selected;
|
||||
|
||||
PostalAddress(@JsonProperty("type") @NonNull Type type,
|
||||
@JsonProperty("label") @Nullable String label,
|
||||
@JsonProperty("street") @Nullable String street,
|
||||
@JsonProperty("poBox") @Nullable String poBox,
|
||||
@JsonProperty("neighborhood") @Nullable String neighborhood,
|
||||
@JsonProperty("city") @Nullable String city,
|
||||
@JsonProperty("region") @Nullable String region,
|
||||
@JsonProperty("postalCode") @Nullable String postalCode,
|
||||
@JsonProperty("country") @Nullable String country)
|
||||
{
|
||||
this.type = type;
|
||||
this.label = label;
|
||||
this.street = street;
|
||||
this.poBox = poBox;
|
||||
this.neighborhood = neighborhood;
|
||||
this.city = city;
|
||||
this.region = region;
|
||||
this.postalCode = postalCode;
|
||||
this.country = country;
|
||||
this.selected = true;
|
||||
}
|
||||
|
||||
private PostalAddress(Parcel in) {
|
||||
this(Type.valueOf(in.readString()),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
in.readString());
|
||||
}
|
||||
|
||||
public @NonNull Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public @Nullable String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public @Nullable String getStreet() {
|
||||
return street;
|
||||
}
|
||||
|
||||
public @Nullable String getPoBox() {
|
||||
return poBox;
|
||||
}
|
||||
|
||||
public @Nullable String getNeighborhood() {
|
||||
return neighborhood;
|
||||
}
|
||||
|
||||
public @Nullable String getCity() {
|
||||
return city;
|
||||
}
|
||||
|
||||
public @Nullable String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public @Nullable String getPostalCode() {
|
||||
return postalCode;
|
||||
}
|
||||
|
||||
public @Nullable String getCountry() {
|
||||
return country;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(type.name());
|
||||
dest.writeString(label);
|
||||
dest.writeString(street);
|
||||
dest.writeString(poBox);
|
||||
dest.writeString(neighborhood);
|
||||
dest.writeString(city);
|
||||
dest.writeString(region);
|
||||
dest.writeString(postalCode);
|
||||
dest.writeString(country);
|
||||
}
|
||||
|
||||
public static final Creator<PostalAddress> CREATOR = new Creator<PostalAddress>() {
|
||||
@Override
|
||||
public PostalAddress createFromParcel(Parcel in) {
|
||||
return new PostalAddress(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PostalAddress[] newArray(int size) {
|
||||
return new PostalAddress[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
if (!TextUtils.isEmpty(street)) {
|
||||
builder.append(street).append('\n');
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(poBox)) {
|
||||
builder.append(poBox).append('\n');
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(neighborhood)) {
|
||||
builder.append(neighborhood).append('\n');
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(city) && !TextUtils.isEmpty(region)) {
|
||||
builder.append(city).append(", ").append(region);
|
||||
} else if (!TextUtils.isEmpty(city)) {
|
||||
builder.append(city).append(' ');
|
||||
} else if (!TextUtils.isEmpty(region)) {
|
||||
builder.append(region).append(' ');
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(postalCode)) {
|
||||
builder.append(postalCode);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(country)) {
|
||||
builder.append('\n').append(country);
|
||||
}
|
||||
|
||||
return builder.toString().trim();
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
HOME, WORK, CUSTOM
|
||||
}
|
||||
}
|
||||
|
||||
public static class Avatar implements Selectable, Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final AttachmentId attachmentId;
|
||||
|
||||
@JsonProperty
|
||||
private final boolean isProfile;
|
||||
|
||||
@JsonIgnore
|
||||
private final Attachment attachment;
|
||||
|
||||
@JsonIgnore
|
||||
private boolean selected;
|
||||
|
||||
public Avatar(@Nullable AttachmentId attachmentId, @Nullable Attachment attachment, boolean isProfile) {
|
||||
this.attachmentId = attachmentId;
|
||||
this.attachment = attachment;
|
||||
this.isProfile = isProfile;
|
||||
this.selected = true;
|
||||
}
|
||||
|
||||
Avatar(@Nullable Uri attachmentUri, boolean isProfile) {
|
||||
this(null, attachmentFromUri(attachmentUri), isProfile);
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
private Avatar(@JsonProperty("attachmentId") @Nullable AttachmentId attachmentId, @JsonProperty("isProfile") boolean isProfile) {
|
||||
this(attachmentId, null, isProfile);
|
||||
}
|
||||
|
||||
private Avatar(Parcel in) {
|
||||
this((Uri) in.readParcelable(Uri.class.getClassLoader()), in.readByte() != 0);
|
||||
}
|
||||
|
||||
public @Nullable AttachmentId getAttachmentId() {
|
||||
return attachmentId;
|
||||
}
|
||||
|
||||
public @Nullable Attachment getAttachment() {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
public boolean isProfile() {
|
||||
return isProfile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static Attachment attachmentFromUri(@Nullable Uri uri) {
|
||||
if (uri == null) return null;
|
||||
return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(attachment != null ? attachment.getDataUri() : null, flags);
|
||||
dest.writeByte((byte) (isProfile ? 1 : 0));
|
||||
}
|
||||
|
||||
public static final Creator<Avatar> CREATOR = new Creator<Avatar>() {
|
||||
@Override
|
||||
public Avatar createFromParcel(Parcel in) {
|
||||
return new Avatar(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Avatar[] newArray(int size) {
|
||||
return new Avatar[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.Phone;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
|
||||
class ContactFieldAdapter extends RecyclerView.Adapter<ContactFieldAdapter.ContactFieldViewHolder> {
|
||||
|
||||
private final Locale locale;
|
||||
private final boolean selectable;
|
||||
private final List<Field> fields;
|
||||
private final GlideRequests glideRequests;
|
||||
|
||||
public ContactFieldAdapter(@NonNull Locale locale, @NonNull GlideRequests glideRequests, boolean selectable) {
|
||||
this.locale = locale;
|
||||
this.glideRequests = glideRequests;
|
||||
this.selectable = selectable;
|
||||
this.fields = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ContactFieldViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ContactFieldViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_selectable_contact_field, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ContactFieldViewHolder holder, int position) {
|
||||
holder.bind(fields.get(position), glideRequests, selectable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull ContactFieldViewHolder holder) {
|
||||
holder.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return fields.size();
|
||||
}
|
||||
|
||||
void setFields(@NonNull Context context,
|
||||
@Nullable Avatar avatar,
|
||||
@NonNull List<Phone> phoneNumbers,
|
||||
@NonNull List<Email> emails,
|
||||
@NonNull List<PostalAddress> postalAddresses)
|
||||
{
|
||||
fields.clear();
|
||||
|
||||
if (avatar != null) {
|
||||
fields.add(new Field(avatar));
|
||||
}
|
||||
|
||||
fields.addAll(Stream.of(phoneNumbers).map(phone -> new Field(context, phone, locale)).toList());
|
||||
fields.addAll(Stream.of(emails).map(email -> new Field(context, email)).toList());
|
||||
fields.addAll(Stream.of(postalAddresses).map(address -> new Field(context, address)).toList());
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class ContactFieldViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final TextView value;
|
||||
private final TextView label;
|
||||
private final ImageView icon;
|
||||
private final ImageView avatar;
|
||||
private final CheckBox checkBox;
|
||||
|
||||
ContactFieldViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
value = itemView.findViewById(R.id.contact_field_value);
|
||||
label = itemView.findViewById(R.id.contact_field_label);
|
||||
icon = itemView.findViewById(R.id.contact_field_icon);
|
||||
avatar = itemView.findViewById(R.id.contact_field_avatar);
|
||||
checkBox = itemView.findViewById(R.id.contact_field_checkbox);
|
||||
}
|
||||
|
||||
void bind(@NonNull Field field, @NonNull GlideRequests glideRequests, boolean selectable) {
|
||||
value.setMaxLines(field.maxLines);
|
||||
value.setText(field.value);
|
||||
label.setText(field.label);
|
||||
icon.setImageResource(field.iconResId);
|
||||
|
||||
if (field.iconUri != null) {
|
||||
avatar.setVisibility(View.VISIBLE);
|
||||
glideRequests.load(field.iconUri).circleCrop().into(avatar);
|
||||
} else {
|
||||
avatar.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (selectable) {
|
||||
checkBox.setVisibility(View.VISIBLE);
|
||||
checkBox.setOnCheckedChangeListener(null);
|
||||
checkBox.setChecked(field.isSelected());
|
||||
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> field.setSelected(isChecked));
|
||||
} else {
|
||||
checkBox.setVisibility(View.GONE);
|
||||
checkBox.setOnCheckedChangeListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
checkBox.setOnCheckedChangeListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
static class Field {
|
||||
|
||||
final String value;
|
||||
final String label;
|
||||
final int iconResId;
|
||||
final int maxLines;
|
||||
final Selectable selectable;
|
||||
|
||||
@Nullable
|
||||
final Uri iconUri;
|
||||
|
||||
Field(@NonNull Context context, @NonNull Phone phoneNumber, @NonNull Locale locale) {
|
||||
this.value = ContactUtil.getPrettyPhoneNumber(phoneNumber, locale);
|
||||
this.iconResId = R.drawable.ic_call_white_24dp;
|
||||
this.iconUri = null;
|
||||
this.maxLines = 1;
|
||||
this.selectable = phoneNumber;
|
||||
|
||||
switch (phoneNumber.getType()) {
|
||||
case HOME:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_home);
|
||||
break;
|
||||
case MOBILE:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_mobile);
|
||||
break;
|
||||
case WORK:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_work);
|
||||
break;
|
||||
case CUSTOM:
|
||||
label = phoneNumber.getLabel() != null ? phoneNumber.getLabel() : "";
|
||||
break;
|
||||
default:
|
||||
label = "";
|
||||
}
|
||||
}
|
||||
|
||||
Field(@NonNull Context context, @NonNull Email email) {
|
||||
this.value = email.getEmail();
|
||||
this.iconResId = R.drawable.baseline_email_white_24;
|
||||
this.iconUri = null;
|
||||
this.maxLines = 1;
|
||||
this.selectable = email;
|
||||
|
||||
switch (email.getType()) {
|
||||
case HOME:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_home);
|
||||
break;
|
||||
case MOBILE:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_mobile);
|
||||
break;
|
||||
case WORK:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_work);
|
||||
break;
|
||||
case CUSTOM:
|
||||
label = email.getLabel() != null ? email.getLabel() : "";
|
||||
break;
|
||||
default:
|
||||
label = "";
|
||||
}
|
||||
}
|
||||
|
||||
Field(@NonNull Context context, @NonNull PostalAddress postalAddress) {
|
||||
this.value = postalAddress.toString();
|
||||
this.iconResId = R.drawable.ic_location_on_white_24dp;
|
||||
this.iconUri = null;
|
||||
this.maxLines = 3;
|
||||
this.selectable = postalAddress;
|
||||
|
||||
switch (postalAddress.getType()) {
|
||||
case HOME:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_home);
|
||||
break;
|
||||
case WORK:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_work);
|
||||
break;
|
||||
case CUSTOM:
|
||||
label = postalAddress.getLabel() != null ? postalAddress.getLabel() : context.getString(R.string.ContactShareEditActivity_type_missing);
|
||||
break;
|
||||
default:
|
||||
label = context.getString(R.string.ContactShareEditActivity_type_missing);
|
||||
}
|
||||
}
|
||||
|
||||
Field(@NonNull Avatar avatar) {
|
||||
this.value = "";
|
||||
this.iconResId = R.drawable.baseline_account_circle_white_24;
|
||||
this.iconUri = avatar.getAttachment() != null ? avatar.getAttachment().getDataUri() : null;
|
||||
this.maxLines = 1;
|
||||
this.selectable = avatar;
|
||||
this.label = "";
|
||||
}
|
||||
|
||||
void setSelected(boolean selected) {
|
||||
selectable.setSelected(selected);
|
||||
}
|
||||
|
||||
boolean isSelected() {
|
||||
return selectable.isSelected();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
|
||||
public class ContactModelMapper {
|
||||
|
||||
public static SharedContact.Builder localToRemoteBuilder(@NonNull Contact contact) {
|
||||
List<SharedContact.Phone> phoneNumbers = new ArrayList<>(contact.getPhoneNumbers().size());
|
||||
List<SharedContact.Email> emails = new ArrayList<>(contact.getEmails().size());
|
||||
List<SharedContact.PostalAddress> postalAddresses = new ArrayList<>(contact.getPostalAddresses().size());
|
||||
|
||||
for (Phone phone : contact.getPhoneNumbers()) {
|
||||
phoneNumbers.add(new SharedContact.Phone.Builder().setValue(phone.getNumber())
|
||||
.setType(localToRemoteType(phone.getType()))
|
||||
.setLabel(phone.getLabel())
|
||||
.build());
|
||||
}
|
||||
|
||||
for (Email email : contact.getEmails()) {
|
||||
emails.add(new SharedContact.Email.Builder().setValue(email.getEmail())
|
||||
.setType(localToRemoteType(email.getType()))
|
||||
.setLabel(email.getLabel())
|
||||
.build());
|
||||
}
|
||||
|
||||
for (PostalAddress postalAddress : contact.getPostalAddresses()) {
|
||||
postalAddresses.add(new SharedContact.PostalAddress.Builder().setType(localToRemoteType(postalAddress.getType()))
|
||||
.setLabel(postalAddress.getLabel())
|
||||
.setStreet(postalAddress.getStreet())
|
||||
.setPobox(postalAddress.getPoBox())
|
||||
.setNeighborhood(postalAddress.getNeighborhood())
|
||||
.setCity(postalAddress.getCity())
|
||||
.setRegion(postalAddress.getRegion())
|
||||
.setPostcode(postalAddress.getPostalCode())
|
||||
.setCountry(postalAddress.getCountry())
|
||||
.build());
|
||||
}
|
||||
|
||||
SharedContact.Name name = new SharedContact.Name.Builder().setDisplay(contact.getName().getDisplayName())
|
||||
.setGiven(contact.getName().getGivenName())
|
||||
.setFamily(contact.getName().getFamilyName())
|
||||
.setPrefix(contact.getName().getPrefix())
|
||||
.setSuffix(contact.getName().getSuffix())
|
||||
.setMiddle(contact.getName().getMiddleName())
|
||||
.build();
|
||||
|
||||
return new SharedContact.Builder().setName(name)
|
||||
.withOrganization(contact.getOrganization())
|
||||
.withPhones(phoneNumbers)
|
||||
.withEmails(emails)
|
||||
.withAddresses(postalAddresses);
|
||||
}
|
||||
|
||||
public static Contact remoteToLocal(@NonNull SharedContact sharedContact) {
|
||||
Name name = new Name(sharedContact.getName().getDisplay().orNull(),
|
||||
sharedContact.getName().getGiven().orNull(),
|
||||
sharedContact.getName().getFamily().orNull(),
|
||||
sharedContact.getName().getPrefix().orNull(),
|
||||
sharedContact.getName().getSuffix().orNull(),
|
||||
sharedContact.getName().getMiddle().orNull());
|
||||
|
||||
List<Phone> phoneNumbers = new LinkedList<>();
|
||||
if (sharedContact.getPhone().isPresent()) {
|
||||
for (SharedContact.Phone phone : sharedContact.getPhone().get()) {
|
||||
phoneNumbers.add(new Phone(phone.getValue(),
|
||||
remoteToLocalType(phone.getType()),
|
||||
phone.getLabel().orNull()));
|
||||
}
|
||||
}
|
||||
|
||||
List<Email> emails = new LinkedList<>();
|
||||
if (sharedContact.getEmail().isPresent()) {
|
||||
for (SharedContact.Email email : sharedContact.getEmail().get()) {
|
||||
emails.add(new Email(email.getValue(),
|
||||
remoteToLocalType(email.getType()),
|
||||
email.getLabel().orNull()));
|
||||
}
|
||||
}
|
||||
|
||||
List<PostalAddress> postalAddresses = new LinkedList<>();
|
||||
if (sharedContact.getAddress().isPresent()) {
|
||||
for (SharedContact.PostalAddress postalAddress : sharedContact.getAddress().get()) {
|
||||
postalAddresses.add(new PostalAddress(remoteToLocalType(postalAddress.getType()),
|
||||
postalAddress.getLabel().orNull(),
|
||||
postalAddress.getStreet().orNull(),
|
||||
postalAddress.getPobox().orNull(),
|
||||
postalAddress.getNeighborhood().orNull(),
|
||||
postalAddress.getCity().orNull(),
|
||||
postalAddress.getRegion().orNull(),
|
||||
postalAddress.getPostcode().orNull(),
|
||||
postalAddress.getCountry().orNull()));
|
||||
}
|
||||
}
|
||||
|
||||
Avatar avatar = null;
|
||||
if (sharedContact.getAvatar().isPresent()) {
|
||||
Attachment attachment = PointerAttachment.forPointer(Optional.of(sharedContact.getAvatar().get().getAttachment().asPointer())).get();
|
||||
boolean isProfile = sharedContact.getAvatar().get().isProfile();
|
||||
|
||||
avatar = new Avatar(null, attachment, isProfile);
|
||||
}
|
||||
|
||||
return new Contact(name, sharedContact.getOrganization().orNull(), phoneNumbers, emails, postalAddresses, avatar);
|
||||
}
|
||||
|
||||
private static Phone.Type remoteToLocalType(SharedContact.Phone.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return Phone.Type.HOME;
|
||||
case MOBILE: return Phone.Type.MOBILE;
|
||||
case WORK: return Phone.Type.WORK;
|
||||
default: return Phone.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static Email.Type remoteToLocalType(SharedContact.Email.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return Email.Type.HOME;
|
||||
case MOBILE: return Email.Type.MOBILE;
|
||||
case WORK: return Email.Type.WORK;
|
||||
default: return Email.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static PostalAddress.Type remoteToLocalType(SharedContact.PostalAddress.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return PostalAddress.Type.HOME;
|
||||
case WORK: return PostalAddress.Type.WORK;
|
||||
default: return PostalAddress.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedContact.Phone.Type localToRemoteType(Phone.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return SharedContact.Phone.Type.HOME;
|
||||
case MOBILE: return SharedContact.Phone.Type.MOBILE;
|
||||
case WORK: return SharedContact.Phone.Type.WORK;
|
||||
default: return SharedContact.Phone.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedContact.Email.Type localToRemoteType(Email.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return SharedContact.Email.Type.HOME;
|
||||
case MOBILE: return SharedContact.Email.Type.MOBILE;
|
||||
case WORK: return SharedContact.Email.Type.WORK;
|
||||
default: return SharedContact.Email.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedContact.PostalAddress.Type localToRemoteType(PostalAddress.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return SharedContact.PostalAddress.Type.HOME;
|
||||
case WORK: return SharedContact.PostalAddress.Type.WORK;
|
||||
default: return SharedContact.PostalAddress.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
|
||||
public class ContactNameEditActivity extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
public static final String KEY_NAME = "name";
|
||||
public static final String KEY_CONTACT_INDEX = "contact_index";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private TextView displayNameView;
|
||||
private ContactNameEditViewModel viewModel;
|
||||
|
||||
static Intent getIntent(@NonNull Context context, @NonNull Name name, int contactPosition) {
|
||||
Intent intent = new Intent(context, ContactNameEditActivity.class);
|
||||
intent.putExtra(KEY_NAME, name);
|
||||
intent.putExtra(KEY_CONTACT_INDEX, contactPosition);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
|
||||
if (getIntent() == null) {
|
||||
throw new IllegalStateException("You must supply extras to this activity. Please use the #getIntent() method.");
|
||||
}
|
||||
|
||||
Name name = getIntent().getParcelableExtra(KEY_NAME);
|
||||
if (name == null) {
|
||||
throw new IllegalStateException("You must supply a name to this activity. Please use the #getIntent() method.");
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_contact_name_edit);
|
||||
|
||||
initializeToolbar();
|
||||
initializeViews(name);
|
||||
|
||||
viewModel = ViewModelProviders.of(this).get(ContactNameEditViewModel.class);
|
||||
viewModel.setName(name);
|
||||
viewModel.getDisplayName().observe(this, displayNameView::setText);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
toolbar.setTitle("");
|
||||
toolbar.setNavigationIcon(R.drawable.ic_check_white_24dp);
|
||||
toolbar.setNavigationOnClickListener(v -> {
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra(KEY_NAME, viewModel.getName());
|
||||
resultIntent.putExtra(KEY_CONTACT_INDEX, getIntent().getIntExtra(KEY_CONTACT_INDEX, -1));
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeViews(@NonNull Name name) {
|
||||
displayNameView = findViewById(R.id.name_edit_display_name);
|
||||
|
||||
TextView givenName = findViewById(R.id.name_edit_given_name);
|
||||
TextView familyName = findViewById(R.id.name_edit_family_name);
|
||||
TextView middleName = findViewById(R.id.name_edit_middle_name);
|
||||
TextView prefix = findViewById(R.id.name_edit_prefix);
|
||||
TextView suffix = findViewById(R.id.name_edit_suffix);
|
||||
|
||||
givenName.setText(name.getGivenName());
|
||||
familyName.setText(name.getFamilyName());
|
||||
middleName.setText(name.getMiddleName());
|
||||
prefix.setText(name.getPrefix());
|
||||
suffix.setText(name.getSuffix());
|
||||
|
||||
givenName.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(String text) {
|
||||
viewModel.updateGivenName(text);
|
||||
}
|
||||
});
|
||||
|
||||
familyName.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(String text) {
|
||||
viewModel.updateFamilyName(text);
|
||||
}
|
||||
});
|
||||
|
||||
middleName.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(String text) {
|
||||
viewModel.updateMiddleName(text);
|
||||
}
|
||||
});
|
||||
|
||||
prefix.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(String text) {
|
||||
viewModel.updatePrefix(text);
|
||||
}
|
||||
});
|
||||
|
||||
suffix.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(String text) {
|
||||
viewModel.updateSuffix(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
|
||||
public class ContactNameEditViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<String> displayName;
|
||||
|
||||
private String givenName;
|
||||
private String familyName;
|
||||
private String middleName;
|
||||
private String prefix;
|
||||
private String suffix;
|
||||
|
||||
public ContactNameEditViewModel() {
|
||||
this.displayName = new MutableLiveData<>();
|
||||
}
|
||||
|
||||
void setName(@NonNull Name name) {
|
||||
givenName = name.getGivenName();
|
||||
familyName = name.getFamilyName();
|
||||
middleName = name.getMiddleName();
|
||||
prefix = name.getPrefix();
|
||||
suffix = name.getSuffix();
|
||||
|
||||
displayName.postValue(buildDisplayName());
|
||||
}
|
||||
|
||||
Name getName() {
|
||||
return new Name(displayName.getValue(), givenName, familyName, prefix, suffix, middleName);
|
||||
}
|
||||
|
||||
LiveData<String> getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
void updateGivenName(@NonNull String givenName) {
|
||||
this.givenName = givenName;
|
||||
displayName.postValue(buildDisplayName());
|
||||
}
|
||||
|
||||
void updateFamilyName(@NonNull String familyName) {
|
||||
this.familyName = familyName;
|
||||
displayName.postValue(buildDisplayName());
|
||||
}
|
||||
|
||||
void updatePrefix(@NonNull String prefix) {
|
||||
this.prefix = prefix;
|
||||
displayName.postValue(buildDisplayName());
|
||||
}
|
||||
|
||||
void updateSuffix(@NonNull String suffix) {
|
||||
this.suffix = suffix;
|
||||
displayName.postValue(buildDisplayName());
|
||||
}
|
||||
|
||||
void updateMiddleName(@NonNull String middleName) {
|
||||
this.middleName = middleName;
|
||||
displayName.postValue(buildDisplayName());
|
||||
}
|
||||
|
||||
private String buildDisplayName() {
|
||||
boolean isCJKV = isCJKV(givenName) && isCJKV(middleName) && isCJKV(familyName) && isCJKV(prefix) && isCJKV(suffix);
|
||||
if (isCJKV) {
|
||||
return joinString(familyName, givenName, prefix, suffix, middleName);
|
||||
}
|
||||
return joinString(prefix, givenName, middleName, familyName, suffix);
|
||||
}
|
||||
|
||||
private String joinString(String... values) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
for (String value : values) {
|
||||
if (!TextUtils.isEmpty(value)) {
|
||||
builder.append(value).append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return builder.toString().trim();
|
||||
}
|
||||
|
||||
private boolean isCJKV(@Nullable String value) {
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int offset = 0; offset < value.length(); ) {
|
||||
int codepoint = Character.codePointAt(value, offset);
|
||||
|
||||
if (!isCodepointCJKV(codepoint)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += Character.charCount(codepoint);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isCodepointCJKV(int codepoint) {
|
||||
if (codepoint == (int)' ') return true;
|
||||
|
||||
Character.UnicodeBlock block = Character.UnicodeBlock.of(codepoint);
|
||||
|
||||
return Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_COMPATIBILITY.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_COMPATIBILITY_FORMS.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_RADICALS_SUPPLEMENT.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION.equals(block) ||
|
||||
Character.UnicodeBlock.ENCLOSED_CJK_LETTERS_AND_MONTHS.equals(block) ||
|
||||
Character.UnicodeBlock.KANGXI_RADICALS.equals(block) ||
|
||||
Character.UnicodeBlock.IDEOGRAPHIC_DESCRIPTION_CHARACTERS.equals(block) ||
|
||||
Character.UnicodeBlock.HIRAGANA.equals(block) ||
|
||||
Character.UnicodeBlock.KATAKANA.equals(block) ||
|
||||
Character.UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS.equals(block) ||
|
||||
Character.UnicodeBlock.HANGUL_JAMO.equals(block) ||
|
||||
Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO.equals(block) ||
|
||||
Character.UnicodeBlock.HANGUL_SYLLABLES.equals(block) ||
|
||||
Character.isIdeographic(codepoint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.ContactsDatabase;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.Email;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.Name;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.Phone;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.PostalAddress;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import ezvcard.Ezvcard;
|
||||
import ezvcard.VCard;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
|
||||
public class ContactRepository {
|
||||
|
||||
private static final String TAG = ContactRepository.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final Executor executor;
|
||||
private final ContactsDatabase contactsDatabase;
|
||||
|
||||
ContactRepository(@NonNull Context context,
|
||||
@NonNull Executor executor,
|
||||
@NonNull ContactsDatabase contactsDatabase)
|
||||
{
|
||||
this.context = context.getApplicationContext();
|
||||
this.executor = executor;
|
||||
this.contactsDatabase = contactsDatabase;
|
||||
}
|
||||
|
||||
void getContacts(@NonNull List<Uri> contactUris, @NonNull ValueCallback<List<Contact>> callback) {
|
||||
executor.execute(() -> {
|
||||
List<Contact> contacts = new ArrayList<>(contactUris.size());
|
||||
for (Uri contactUri : contactUris) {
|
||||
Contact contact;
|
||||
|
||||
if (ContactsContract.AUTHORITY.equals(contactUri.getAuthority())) {
|
||||
contact = getContactFromSystemContacts(ContactUtil.getContactIdFromUri(contactUri));
|
||||
} else {
|
||||
contact = getContactFromVcard(contactUri);
|
||||
}
|
||||
|
||||
if (contact != null) {
|
||||
contacts.add(contact);
|
||||
}
|
||||
}
|
||||
callback.onComplete(contacts);
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable Contact getContactFromSystemContacts(long contactId) {
|
||||
Name name = getName(contactId);
|
||||
if (name == null) {
|
||||
Log.w(TAG, "Couldn't find a name associated with the provided contact ID.");
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Phone> phoneNumbers = getPhoneNumbers(contactId);
|
||||
AvatarInfo avatarInfo = getAvatarInfo(contactId, phoneNumbers);
|
||||
Avatar avatar = avatarInfo != null ? new Avatar(avatarInfo.uri, avatarInfo.isProfile) : null;
|
||||
|
||||
return new Contact(name, null, phoneNumbers, getEmails(contactId), getPostalAddresses(contactId), avatar);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable Contact getContactFromVcard(@NonNull Uri uri) {
|
||||
Contact contact = null;
|
||||
|
||||
try (InputStream stream = PartAuthority.getAttachmentStream(context, uri)) {
|
||||
VCard vcard = Ezvcard.parse(stream).first();
|
||||
|
||||
ezvcard.property.StructuredName vName = vcard.getStructuredName();
|
||||
List<ezvcard.property.Telephone> vPhones = vcard.getTelephoneNumbers();
|
||||
List<ezvcard.property.Email> vEmails = vcard.getEmails();
|
||||
List<ezvcard.property.Address> vPostalAddresses = vcard.getAddresses();
|
||||
|
||||
String organization = vcard.getOrganization() != null && !vcard.getOrganization().getValues().isEmpty() ? vcard.getOrganization().getValues().get(0) : null;
|
||||
String displayName = vcard.getFormattedName() != null ? vcard.getFormattedName().getValue() : null;
|
||||
|
||||
if (displayName == null && vName != null) {
|
||||
displayName = vName.getGiven();
|
||||
}
|
||||
|
||||
if (displayName == null && vcard.getOrganization() != null) {
|
||||
displayName = organization;
|
||||
}
|
||||
|
||||
if (displayName == null) {
|
||||
throw new IOException("No valid name.");
|
||||
}
|
||||
|
||||
Name name = new Name(displayName,
|
||||
vName != null ? vName.getGiven() : null,
|
||||
vName != null ? vName.getFamily() : null,
|
||||
vName != null && !vName.getPrefixes().isEmpty() ? vName.getPrefixes().get(0) : null,
|
||||
vName != null && !vName.getSuffixes().isEmpty() ? vName.getSuffixes().get(0) : null,
|
||||
null);
|
||||
|
||||
|
||||
List<Phone> phoneNumbers = new ArrayList<>(vPhones.size());
|
||||
for (ezvcard.property.Telephone vEmail : vPhones) {
|
||||
String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
|
||||
phoneNumbers.add(new Phone(vEmail.getText(), phoneTypeFromVcardType(label), label));
|
||||
}
|
||||
|
||||
List<Email> emails = new ArrayList<>(vEmails.size());
|
||||
for (ezvcard.property.Email vEmail : vEmails) {
|
||||
String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
|
||||
emails.add(new Email(vEmail.getValue(), emailTypeFromVcardType(label), label));
|
||||
}
|
||||
|
||||
List<PostalAddress> postalAddresses = new ArrayList<>(vPostalAddresses.size());
|
||||
for (ezvcard.property.Address vPostalAddress : vPostalAddresses) {
|
||||
String label = !vPostalAddress.getTypes().isEmpty() ? getCleanedVcardType(vPostalAddress.getTypes().get(0).getValue()) : null;
|
||||
postalAddresses.add(new PostalAddress(postalAddressTypeFromVcardType(label),
|
||||
label,
|
||||
vPostalAddress.getStreetAddress(),
|
||||
vPostalAddress.getPoBox(),
|
||||
null,
|
||||
vPostalAddress.getLocality(),
|
||||
vPostalAddress.getRegion(),
|
||||
vPostalAddress.getPostalCode(),
|
||||
vPostalAddress.getCountry()));
|
||||
}
|
||||
|
||||
contact = new Contact(name, organization, phoneNumbers, emails, postalAddresses, null);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to parse the vcard.", e);
|
||||
}
|
||||
|
||||
if (BlobProvider.AUTHORITY.equals(uri.getAuthority())) {
|
||||
BlobProvider.getInstance().delete(context, uri);
|
||||
}
|
||||
|
||||
return contact;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable Name getName(long contactId) {
|
||||
try (Cursor cursor = contactsDatabase.getNameDetails(contactId)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
String cursorDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME));
|
||||
String cursorGivenName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME));
|
||||
String cursorFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME));
|
||||
String cursorPrefix = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.PREFIX));
|
||||
String cursorSuffix = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.SUFFIX));
|
||||
String cursorMiddleName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME));
|
||||
|
||||
Name name = new Name(cursorDisplayName, cursorGivenName, cursorFamilyName, cursorPrefix, cursorSuffix, cursorMiddleName);
|
||||
if (!name.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String org = contactsDatabase.getOrganizationName(contactId);
|
||||
if (!TextUtils.isEmpty(org)) {
|
||||
return new Name(org, org, null, null, null, null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<Phone> getPhoneNumbers(long contactId) {
|
||||
Map<String, Phone> numberMap = new HashMap<>();
|
||||
try (Cursor cursor = contactsDatabase.getPhoneDetails(contactId)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String cursorNumber = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));
|
||||
int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE));
|
||||
String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL));
|
||||
|
||||
String number = ContactUtil.getNormalizedPhoneNumber(context, cursorNumber);
|
||||
Phone existing = numberMap.get(number);
|
||||
Phone candidate = new Phone(number, phoneTypeFromContactType(cursorType), cursorLabel);
|
||||
|
||||
if (existing == null || (existing.getType() == Phone.Type.CUSTOM && existing.getLabel() == null)) {
|
||||
numberMap.put(number, candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Phone> numbers = new ArrayList<>(numberMap.size());
|
||||
numbers.addAll(numberMap.values());
|
||||
return numbers;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<Email> getEmails(long contactId) {
|
||||
List<Email> emails = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = contactsDatabase.getEmailDetails(contactId)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String cursorEmail = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.ADDRESS));
|
||||
int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.TYPE));
|
||||
String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.LABEL));
|
||||
|
||||
emails.add(new Email(cursorEmail, emailTypeFromContactType(cursorType), cursorLabel));
|
||||
}
|
||||
}
|
||||
|
||||
return emails;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<PostalAddress> getPostalAddresses(long contactId) {
|
||||
List<PostalAddress> postalAddresses = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = contactsDatabase.getPostalAddressDetails(contactId)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.TYPE));
|
||||
String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.LABEL));
|
||||
String cursorStreet = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.STREET));
|
||||
String cursorPoBox = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.POBOX));
|
||||
String cursorNeighborhood = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD));
|
||||
String cursorCity = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.CITY));
|
||||
String cursorRegion = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.REGION));
|
||||
String cursorPostal = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE));
|
||||
String cursorCountry = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY));
|
||||
|
||||
postalAddresses.add(new PostalAddress(postalAddressTypeFromContactType(cursorType),
|
||||
cursorLabel,
|
||||
cursorStreet,
|
||||
cursorPoBox,
|
||||
cursorNeighborhood,
|
||||
cursorCity,
|
||||
cursorRegion,
|
||||
cursorPostal,
|
||||
cursorCountry));
|
||||
}
|
||||
}
|
||||
|
||||
return postalAddresses;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable AvatarInfo getAvatarInfo(long contactId, List<Phone> phoneNumbers) {
|
||||
AvatarInfo systemAvatar = getSystemAvatarInfo(contactId);
|
||||
|
||||
if (systemAvatar != null) {
|
||||
return systemAvatar;
|
||||
}
|
||||
|
||||
for (Phone phoneNumber : phoneNumbers) {
|
||||
AvatarInfo recipientAvatar = getRecipientAvatarInfo(Address.fromExternal(context, phoneNumber.getNumber()));
|
||||
if (recipientAvatar != null) {
|
||||
return recipientAvatar;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable AvatarInfo getSystemAvatarInfo(long contactId) {
|
||||
Uri uri = contactsDatabase.getAvatarUri(contactId);
|
||||
if (uri != null) {
|
||||
return new AvatarInfo(uri, false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable AvatarInfo getRecipientAvatarInfo(@NonNull Address address) {
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
ContactPhoto contactPhoto = recipient.getContactPhoto();
|
||||
|
||||
if (contactPhoto != null) {
|
||||
Uri avatarUri = contactPhoto.getUri(context);
|
||||
if (avatarUri != null) {
|
||||
return new AvatarInfo(avatarUri, contactPhoto.isProfilePhoto());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Phone.Type phoneTypeFromContactType(int type) {
|
||||
switch (type) {
|
||||
case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
|
||||
return Phone.Type.HOME;
|
||||
case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
|
||||
return Phone.Type.MOBILE;
|
||||
case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
|
||||
return Phone.Type.WORK;
|
||||
}
|
||||
return Phone.Type.CUSTOM;
|
||||
}
|
||||
|
||||
private Phone.Type phoneTypeFromVcardType(@Nullable String type) {
|
||||
if ("home".equalsIgnoreCase(type)) return Phone.Type.HOME;
|
||||
else if ("cell".equalsIgnoreCase(type)) return Phone.Type.MOBILE;
|
||||
else if ("work".equalsIgnoreCase(type)) return Phone.Type.WORK;
|
||||
else return Phone.Type.CUSTOM;
|
||||
}
|
||||
|
||||
private Email.Type emailTypeFromContactType(int type) {
|
||||
switch (type) {
|
||||
case ContactsContract.CommonDataKinds.Email.TYPE_HOME:
|
||||
return Email.Type.HOME;
|
||||
case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE:
|
||||
return Email.Type.MOBILE;
|
||||
case ContactsContract.CommonDataKinds.Email.TYPE_WORK:
|
||||
return Email.Type.WORK;
|
||||
}
|
||||
return Email.Type.CUSTOM;
|
||||
}
|
||||
|
||||
private Email.Type emailTypeFromVcardType(@Nullable String type) {
|
||||
if ("home".equalsIgnoreCase(type)) return Email.Type.HOME;
|
||||
else if ("cell".equalsIgnoreCase(type)) return Email.Type.MOBILE;
|
||||
else if ("work".equalsIgnoreCase(type)) return Email.Type.WORK;
|
||||
else return Email.Type.CUSTOM;
|
||||
}
|
||||
|
||||
private PostalAddress.Type postalAddressTypeFromContactType(int type) {
|
||||
switch (type) {
|
||||
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME:
|
||||
return PostalAddress.Type.HOME;
|
||||
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK:
|
||||
return PostalAddress.Type.WORK;
|
||||
}
|
||||
return PostalAddress.Type.CUSTOM;
|
||||
}
|
||||
|
||||
private PostalAddress.Type postalAddressTypeFromVcardType(@Nullable String type) {
|
||||
if ("home".equalsIgnoreCase(type)) return PostalAddress.Type.HOME;
|
||||
else if ("work".equalsIgnoreCase(type)) return PostalAddress.Type.WORK;
|
||||
else return PostalAddress.Type.CUSTOM;
|
||||
}
|
||||
|
||||
private String getCleanedVcardType(@Nullable String type) {
|
||||
if (TextUtils.isEmpty(type)) return "";
|
||||
|
||||
if (type.startsWith("x-") && type.length() > 2) {
|
||||
return type.substring(2);
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
interface ValueCallback<T> {
|
||||
void onComplete(@NonNull T value);
|
||||
}
|
||||
|
||||
private static class AvatarInfo {
|
||||
|
||||
private final Uri uri;
|
||||
private final boolean isProfile;
|
||||
|
||||
private AvatarInfo(Uri uri, boolean isProfile) {
|
||||
this.uri = uri;
|
||||
this.isProfile = isProfile;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public boolean isProfile() {
|
||||
return isProfile;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.app.Activity;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
import static org.thoughtcrime.securesms.contactshare.ContactShareEditViewModel.*;
|
||||
|
||||
public class ContactShareEditActivity extends PassphraseRequiredActionBarActivity implements ContactShareEditAdapter.EventListener {
|
||||
|
||||
public static final String KEY_CONTACTS = "contacts";
|
||||
private static final String KEY_CONTACT_URIS = "contact_uris";
|
||||
private static final int CODE_NAME_EDIT = 55;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private ContactShareEditViewModel viewModel;
|
||||
|
||||
public static Intent getIntent(@NonNull Context context, @NonNull List<Uri> contactUris) {
|
||||
ArrayList<Uri> contactUriList = new ArrayList<>(contactUris);
|
||||
|
||||
Intent intent = new Intent(context, ContactShareEditActivity.class);
|
||||
intent.putParcelableArrayListExtra(KEY_CONTACT_URIS, contactUriList);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState, boolean ready) {
|
||||
setContentView(R.layout.activity_contact_share_edit);
|
||||
|
||||
if (getIntent() == null) {
|
||||
throw new IllegalStateException("You must supply extras to this activity. Please use the #getIntent() method.");
|
||||
}
|
||||
|
||||
List<Uri> contactUris = getIntent().getParcelableArrayListExtra(KEY_CONTACT_URIS);
|
||||
if (contactUris == null) {
|
||||
throw new IllegalStateException("You must supply contact Uri's to this activity. Please use the #getIntent() method.");
|
||||
}
|
||||
|
||||
View sendButton = findViewById(R.id.contact_share_edit_send);
|
||||
sendButton.setOnClickListener(v -> onSendClicked(viewModel.getFinalizedContacts()));
|
||||
|
||||
RecyclerView contactList = findViewById(R.id.contact_share_edit_list);
|
||||
contactList.setLayoutManager(new LinearLayoutManager(this));
|
||||
contactList.getLayoutManager().setAutoMeasureEnabled(true);
|
||||
|
||||
ContactShareEditAdapter contactAdapter = new ContactShareEditAdapter(GlideApp.with(this), dynamicLanguage.getCurrentLocale(), this);
|
||||
contactList.setAdapter(contactAdapter);
|
||||
|
||||
ContactRepository contactRepository = new ContactRepository(this,
|
||||
AsyncTask.THREAD_POOL_EXECUTOR,
|
||||
DatabaseFactory.getContactsDatabase(this));
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new Factory(contactUris, contactRepository)).get(ContactShareEditViewModel.class);
|
||||
viewModel.getContacts().observe(this, contacts -> {
|
||||
contactAdapter.setContacts(contacts);
|
||||
contactList.post(() -> contactList.scrollToPosition(0));
|
||||
});
|
||||
viewModel.getEvents().observe(this, this::presentEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private void presentEvent(@Nullable Event event) {
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event == Event.BAD_CONTACT) {
|
||||
Toast.makeText(this, R.string.ContactShareEditActivity_invalid_contact, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void onSendClicked(List<Contact> contacts) {
|
||||
Intent intent = new Intent();
|
||||
|
||||
ArrayList<Contact> contactArrayList = new ArrayList<>(contacts.size());
|
||||
contactArrayList.addAll(contacts);
|
||||
intent.putExtra(KEY_CONTACTS, contactArrayList);
|
||||
|
||||
setResult(Activity.RESULT_OK, intent);
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNameEditClicked(int position, @NonNull Name name) {
|
||||
startActivityForResult(ContactNameEditActivity.getIntent(this, name, position), CODE_NAME_EDIT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode != CODE_NAME_EDIT || resultCode != RESULT_OK || data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int position = data.getIntExtra(ContactNameEditActivity.KEY_CONTACT_INDEX, -1);
|
||||
Name name = data.getParcelableExtra(ContactNameEditActivity.KEY_NAME);
|
||||
|
||||
if (name != null) {
|
||||
viewModel.updateContactName(position, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
|
||||
public class ContactShareEditAdapter extends RecyclerView.Adapter<ContactShareEditAdapter.ContactEditViewHolder> {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final Locale locale;
|
||||
private final EventListener eventListener;
|
||||
private final List<Contact> contacts;
|
||||
|
||||
ContactShareEditAdapter(@NonNull GlideRequests glideRequests, @NonNull Locale locale, @NonNull EventListener eventListener) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.locale = locale;
|
||||
this.eventListener = eventListener;
|
||||
this.contacts = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ContactEditViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ContactEditViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_editable_contact, parent, false),
|
||||
locale,
|
||||
glideRequests);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ContactEditViewHolder holder, int position) {
|
||||
holder.bind(position, contacts.get(position), eventListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return contacts.size();
|
||||
}
|
||||
|
||||
void setContacts(@Nullable List<Contact> contacts) {
|
||||
this.contacts.clear();
|
||||
|
||||
if (contacts != null) {
|
||||
this.contacts.addAll(contacts);
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class ContactEditViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final TextView name;
|
||||
private final View nameEditButton;
|
||||
private final ContactFieldAdapter fieldAdapter;
|
||||
|
||||
ContactEditViewHolder(View itemView, @NonNull Locale locale, @NonNull GlideRequests glideRequests) {
|
||||
super(itemView);
|
||||
|
||||
this.name = itemView.findViewById(R.id.editable_contact_name);
|
||||
this.nameEditButton = itemView.findViewById(R.id.editable_contact_name_edit_button);
|
||||
this.fieldAdapter = new ContactFieldAdapter(locale, glideRequests, true);
|
||||
|
||||
RecyclerView fields = itemView.findViewById(R.id.editable_contact_fields);
|
||||
fields.setLayoutManager(new LinearLayoutManager(itemView.getContext()));
|
||||
fields.getLayoutManager().setAutoMeasureEnabled(true);
|
||||
fields.setAdapter(fieldAdapter);
|
||||
}
|
||||
|
||||
void bind(int position, @NonNull Contact contact, @NonNull EventListener eventListener) {
|
||||
Context context = itemView.getContext();
|
||||
|
||||
name.setText(ContactUtil.getDisplayName(contact));
|
||||
nameEditButton.setOnClickListener(v -> eventListener.onNameEditClicked(position, contact.getName()));
|
||||
fieldAdapter.setFields(context, contact.getAvatar(), contact.getPhoneNumbers(), contact.getEmails(), contact.getPostalAddresses());
|
||||
}
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onNameEditClicked(int position, @NonNull Name name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.Name;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class ContactShareEditViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<List<Contact>> contacts;
|
||||
private final SingleLiveEvent<Event> events;
|
||||
private final ContactRepository repo;
|
||||
|
||||
ContactShareEditViewModel(@NonNull List<Uri> contactUris,
|
||||
@NonNull ContactRepository contactRepository)
|
||||
{
|
||||
contacts = new MutableLiveData<>();
|
||||
events = new SingleLiveEvent<>();
|
||||
repo = contactRepository;
|
||||
|
||||
repo.getContacts(contactUris, retrieved -> {
|
||||
if (retrieved.isEmpty()) {
|
||||
events.postValue(Event.BAD_CONTACT);
|
||||
} else {
|
||||
contacts.postValue(retrieved);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<Contact>> getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
@NonNull List<Contact> getFinalizedContacts() {
|
||||
List<Contact> currentContacts = getCurrentContacts();
|
||||
List<Contact> trimmedContacts = new ArrayList<>(currentContacts.size());
|
||||
|
||||
for (Contact contact : currentContacts) {
|
||||
Contact trimmed = new Contact(contact.getName(),
|
||||
contact.getOrganization(),
|
||||
trimSelectables(contact.getPhoneNumbers()),
|
||||
trimSelectables(contact.getEmails()),
|
||||
trimSelectables(contact.getPostalAddresses()),
|
||||
contact.getAvatar() != null && contact.getAvatar().isSelected() ? contact.getAvatar() : null);
|
||||
trimmedContacts.add(trimmed);
|
||||
}
|
||||
|
||||
return trimmedContacts;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Event> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
void updateContactName(int contactPosition, @NonNull Name name) {
|
||||
if (name.isEmpty()) {
|
||||
events.postValue(Event.BAD_CONTACT);
|
||||
return;
|
||||
}
|
||||
|
||||
List<Contact> currentContacts = getCurrentContacts();
|
||||
Contact original = currentContacts.remove(contactPosition);
|
||||
|
||||
currentContacts.add(new Contact(name,
|
||||
original.getOrganization(),
|
||||
original.getPhoneNumbers(),
|
||||
original.getEmails(),
|
||||
original.getPostalAddresses(),
|
||||
original.getAvatar()));
|
||||
|
||||
contacts.postValue(currentContacts);
|
||||
}
|
||||
|
||||
private <E extends Selectable> List<E> trimSelectables(List<E> selectables) {
|
||||
return Stream.of(selectables).filter(Selectable::isSelected).toList();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<Contact> getCurrentContacts() {
|
||||
List<Contact> currentContacts = contacts.getValue();
|
||||
return currentContacts != null ? currentContacts : new ArrayList<>();
|
||||
}
|
||||
|
||||
enum Event {
|
||||
BAD_CONTACT
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
|
||||
private final List<Uri> contactUris;
|
||||
private final ContactRepository contactRepository;
|
||||
|
||||
Factory(@NonNull List<Uri> contactUris, @NonNull ContactRepository contactRepository) {
|
||||
this.contactUris = contactUris;
|
||||
this.contactRepository = contactRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return modelClass.cast(new ContactShareEditViewModel(contactUris, contactRepository));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.Email;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.Phone;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact.PostalAddress;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public final class ContactUtil {
|
||||
|
||||
private static final String TAG = ContactUtil.class.getSimpleName();
|
||||
|
||||
public static long getContactIdFromUri(@NonNull Uri uri) {
|
||||
try {
|
||||
return Long.parseLong(uri.getLastPathSegment());
|
||||
} catch (NumberFormatException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull CharSequence getStringSummary(@NonNull Context context, @NonNull Contact contact) {
|
||||
String contactName = ContactUtil.getDisplayName(contact);
|
||||
|
||||
if (!TextUtils.isEmpty(contactName)) {
|
||||
return context.getString(R.string.MessageNotifier_contact_message, EmojiStrings.BUST_IN_SILHOUETTE, contactName);
|
||||
}
|
||||
|
||||
return SpanUtil.italic(context.getString(R.string.MessageNotifier_unknown_contact_message));
|
||||
}
|
||||
|
||||
public static @NonNull String getDisplayName(@Nullable Contact contact) {
|
||||
if (contact == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(contact.getName().getDisplayName())) {
|
||||
return contact.getName().getDisplayName();
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(contact.getOrganization())) {
|
||||
return contact.getOrganization();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public static @NonNull String getDisplayNumber(@NonNull Contact contact, @NonNull Locale locale) {
|
||||
Phone displayNumber = getPrimaryNumber(contact);
|
||||
|
||||
if (displayNumber != null) {
|
||||
return ContactUtil.getPrettyPhoneNumber(displayNumber, locale);
|
||||
} else if (contact.getEmails().size() > 0) {
|
||||
return contact.getEmails().get(0).getEmail();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable Phone getPrimaryNumber(@NonNull Contact contact) {
|
||||
if (contact.getPhoneNumbers().size() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Phone> mobileNumbers = Stream.of(contact.getPhoneNumbers()).filter(number -> number.getType() == Phone.Type.MOBILE).toList();
|
||||
if (mobileNumbers.size() > 0) {
|
||||
return mobileNumbers.get(0);
|
||||
}
|
||||
|
||||
return contact.getPhoneNumbers().get(0);
|
||||
}
|
||||
|
||||
public static @NonNull String getPrettyPhoneNumber(@NonNull Phone phoneNumber, @NonNull Locale fallbackLocale) {
|
||||
return getPrettyPhoneNumber(phoneNumber.getNumber(), fallbackLocale);
|
||||
}
|
||||
|
||||
private static @NonNull String getPrettyPhoneNumber(@NonNull String phoneNumber, @NonNull Locale fallbackLocale) {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public static @NonNull String getNormalizedPhoneNumber(@NonNull Context context, @NonNull String number) {
|
||||
Address address = Address.fromExternal(context, number);
|
||||
return address.serialize();
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public static void selectRecipientThroughDialog(@NonNull Context context, @NonNull List<Recipient> choices, @NonNull Locale locale, @NonNull RecipientSelectedCallback callback) {
|
||||
if (choices.size() > 1) {
|
||||
CharSequence[] values = new CharSequence[choices.size()];
|
||||
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = getPrettyPhoneNumber(choices.get(i).getAddress().toPhoneString(), locale);
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setItems(values, ((dialog, which) -> callback.onSelected(choices.get(which))))
|
||||
.create()
|
||||
.show();
|
||||
} else {
|
||||
callback.onSelected(choices.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Recipient> getRecipients(@NonNull Context context, @NonNull Contact contact) {
|
||||
return Stream.of(contact.getPhoneNumbers()).map(phone -> Recipient.from(context, Address.fromExternal(context, phone.getNumber()), true)).toList();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull Intent buildAddToContactsIntent(@NonNull Context context, @NonNull Contact contact) {
|
||||
Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
|
||||
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
|
||||
|
||||
if (!TextUtils.isEmpty(contact.getName().getDisplayName())) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.NAME, contact.getName().getDisplayName());
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(contact.getOrganization())) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.COMPANY, contact.getOrganization());
|
||||
}
|
||||
|
||||
if (contact.getPhoneNumbers().size() > 0) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.PHONE, contact.getPhoneNumbers().get(0).getNumber());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.PHONE_TYPE, getSystemType(contact.getPhoneNumbers().get(0).getType()));
|
||||
}
|
||||
|
||||
if (contact.getPhoneNumbers().size() > 1) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.SECONDARY_PHONE, contact.getPhoneNumbers().get(1).getNumber());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE, getSystemType(contact.getPhoneNumbers().get(1).getType()));
|
||||
}
|
||||
|
||||
if (contact.getPhoneNumbers().size() > 2) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.TERTIARY_PHONE, contact.getPhoneNumbers().get(2).getNumber());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE, getSystemType(contact.getPhoneNumbers().get(2).getType()));
|
||||
}
|
||||
|
||||
if (contact.getEmails().size() > 0) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.EMAIL, contact.getEmails().get(0).getEmail());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.EMAIL_TYPE, getSystemType(contact.getEmails().get(0).getType()));
|
||||
}
|
||||
|
||||
if (contact.getEmails().size() > 1) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.SECONDARY_EMAIL, contact.getEmails().get(1).getEmail());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE, getSystemType(contact.getEmails().get(1).getType()));
|
||||
}
|
||||
|
||||
if (contact.getEmails().size() > 2) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.TERTIARY_EMAIL, contact.getEmails().get(2).getEmail());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE, getSystemType(contact.getEmails().get(2).getType()));
|
||||
}
|
||||
|
||||
if (contact.getPostalAddresses().size() > 0) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.POSTAL, contact.getPostalAddresses().get(0).toString());
|
||||
intent.putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, getSystemType(contact.getPostalAddresses().get(0).getType()));
|
||||
}
|
||||
|
||||
if (contact.getAvatarAttachment() != null && contact.getAvatarAttachment().getDataUri() != null) {
|
||||
try {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
|
||||
values.put(ContactsContract.CommonDataKinds.Photo.PHOTO, Util.readFully(PartAuthority.getAttachmentStream(context, contact.getAvatarAttachment().getDataUri())));
|
||||
|
||||
ArrayList<ContentValues> valuesArray = new ArrayList<>(1);
|
||||
valuesArray.add(values);
|
||||
|
||||
intent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, valuesArray);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to read avatar into a byte array.", e);
|
||||
}
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
private static int getSystemType(Phone.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return ContactsContract.CommonDataKinds.Phone.TYPE_HOME;
|
||||
case MOBILE: return ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE;
|
||||
case WORK: return ContactsContract.CommonDataKinds.Phone.TYPE_WORK;
|
||||
default: return ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getSystemType(Email.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return ContactsContract.CommonDataKinds.Email.TYPE_HOME;
|
||||
case MOBILE: return ContactsContract.CommonDataKinds.Email.TYPE_MOBILE;
|
||||
case WORK: return ContactsContract.CommonDataKinds.Email.TYPE_WORK;
|
||||
default: return ContactsContract.CommonDataKinds.Email.TYPE_CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getSystemType(PostalAddress.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME;
|
||||
case WORK: return ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK;
|
||||
default: return ContactsContract.CommonDataKinds.StructuredPostal.TYPE_CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
public interface RecipientSelectedCallback {
|
||||
void onSelected(@NonNull Recipient recipient);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
public interface Selectable {
|
||||
void setSelected(boolean selected);
|
||||
boolean isSelected();
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
import static org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
|
||||
public class SharedContactDetailsActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener {
|
||||
|
||||
private static final int CODE_ADD_EDIT_CONTACT = 2323;
|
||||
private static final String KEY_CONTACT = "contact";
|
||||
|
||||
private ContactFieldAdapter contactFieldAdapter;
|
||||
private TextView nameView;
|
||||
private TextView numberView;
|
||||
private ImageView avatarView;
|
||||
private View addButtonView;
|
||||
private View inviteButtonView;
|
||||
private ViewGroup engageContainerView;
|
||||
private View messageButtonView;
|
||||
private View callButtonView;
|
||||
|
||||
private GlideRequests glideRequests;
|
||||
private Contact contact;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private final Map<String, Recipient> activeRecipients = new HashMap<>();
|
||||
|
||||
public static Intent getIntent(@NonNull Context context, @NonNull Contact contact) {
|
||||
Intent intent = new Intent(context, SharedContactDetailsActivity.class);
|
||||
intent.putExtra(KEY_CONTACT, contact);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState, boolean ready) {
|
||||
setContentView(R.layout.activity_shared_contact_details);
|
||||
|
||||
if (getIntent() == null) {
|
||||
throw new IllegalStateException("You must supply arguments to this activity. Please use the #getIntent() method.");
|
||||
}
|
||||
|
||||
contact = getIntent().getParcelableExtra(KEY_CONTACT);
|
||||
if (contact == null) {
|
||||
throw new IllegalStateException("You must supply a contact to this activity. Please use the #getIntent() method.");
|
||||
}
|
||||
|
||||
initToolbar();
|
||||
initViews();
|
||||
|
||||
presentContact(contact);
|
||||
presentActionButtons(ContactUtil.getRecipients(this, contact));
|
||||
presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getDataUri() : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private void initToolbar() {
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setLogo(null);
|
||||
getSupportActionBar().setTitle("");
|
||||
toolbar.setNavigationOnClickListener(v -> onBackPressed());
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
int[] attrs = {R.attr.shared_contact_details_titlebar};
|
||||
TypedArray array = obtainStyledAttributes(attrs);
|
||||
int color = array.getResourceId(0, android.R.color.black);
|
||||
|
||||
array.recycle();
|
||||
|
||||
getWindow().setStatusBarColor(getResources().getColor(color));
|
||||
}
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
nameView = findViewById(R.id.contact_details_name);
|
||||
numberView = findViewById(R.id.contact_details_number);
|
||||
avatarView = findViewById(R.id.contact_details_avatar);
|
||||
addButtonView = findViewById(R.id.contact_details_add_button);
|
||||
inviteButtonView = findViewById(R.id.contact_details_invite_button);
|
||||
engageContainerView = findViewById(R.id.contact_details_engage_container);
|
||||
messageButtonView = findViewById(R.id.contact_details_message_button);
|
||||
callButtonView = findViewById(R.id.contact_details_call_button);
|
||||
|
||||
contactFieldAdapter = new ContactFieldAdapter(dynamicLanguage.getCurrentLocale(), glideRequests, false);
|
||||
|
||||
RecyclerView list = findViewById(R.id.contact_details_fields);
|
||||
list.setLayoutManager(new LinearLayoutManager(this));
|
||||
list.setAdapter(contactFieldAdapter);
|
||||
|
||||
glideRequests = GlideApp.with(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModified(Recipient recipient) {
|
||||
Util.runOnMain(() -> presentActionButtons(Collections.singletonList(recipient)));
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void presentContact(@Nullable Contact contact) {
|
||||
this.contact = contact;
|
||||
|
||||
if (contact != null) {
|
||||
nameView.setText(ContactUtil.getDisplayName(contact));
|
||||
numberView.setText(ContactUtil.getDisplayNumber(contact, dynamicLanguage.getCurrentLocale()));
|
||||
|
||||
addButtonView.setOnClickListener(v -> {
|
||||
new AsyncTask<Void, Void, Intent>() {
|
||||
@Override
|
||||
protected Intent doInBackground(Void... voids) {
|
||||
return ContactUtil.buildAddToContactsIntent(SharedContactDetailsActivity.this, contact);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Intent intent) {
|
||||
startActivityForResult(intent, CODE_ADD_EDIT_CONTACT);
|
||||
}
|
||||
}.execute();
|
||||
});
|
||||
|
||||
contactFieldAdapter.setFields(this, null, contact.getPhoneNumbers(), contact.getEmails(), contact.getPostalAddresses());
|
||||
} else {
|
||||
nameView.setText("");
|
||||
numberView.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
public void presentAvatar(@Nullable Uri uri) {
|
||||
if (uri != null) {
|
||||
glideRequests.load(new DecryptableUri(uri))
|
||||
.fallback(R.drawable.ic_contact_picture)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(avatarView);
|
||||
} else {
|
||||
glideRequests.load(R.drawable.ic_contact_picture)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(avatarView);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentActionButtons(@NonNull List<Recipient> recipients) {
|
||||
for (Recipient recipient : recipients) {
|
||||
activeRecipients.put(recipient.getAddress().serialize(), recipient);
|
||||
}
|
||||
|
||||
List<Recipient> pushUsers = new ArrayList<>(recipients.size());
|
||||
List<Recipient> systemUsers = new ArrayList<>(recipients.size());
|
||||
|
||||
for (Recipient recipient : activeRecipients.values()) {
|
||||
recipient.addListener(this);
|
||||
|
||||
if (recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
|
||||
pushUsers.add(recipient);
|
||||
} else if (recipient.isSystemContact()) {
|
||||
systemUsers.add(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pushUsers.isEmpty()) {
|
||||
engageContainerView.setVisibility(View.VISIBLE);
|
||||
inviteButtonView.setVisibility(View.GONE);
|
||||
|
||||
messageButtonView.setOnClickListener(v -> {
|
||||
ContactUtil.selectRecipientThroughDialog(this, pushUsers, dynamicLanguage.getCurrentLocale(), recipient -> {
|
||||
CommunicationActions.startConversation(this, recipient, null);
|
||||
});
|
||||
});
|
||||
|
||||
callButtonView.setOnClickListener(v -> {
|
||||
ContactUtil.selectRecipientThroughDialog(this, pushUsers, dynamicLanguage.getCurrentLocale(), recipient -> CommunicationActions.startVoiceCall(this, recipient));
|
||||
});
|
||||
} else if (!systemUsers.isEmpty()) {
|
||||
inviteButtonView.setVisibility(View.VISIBLE);
|
||||
engageContainerView.setVisibility(View.GONE);
|
||||
|
||||
inviteButtonView.setOnClickListener(v -> {
|
||||
ContactUtil.selectRecipientThroughDialog(this, systemUsers, dynamicLanguage.getCurrentLocale(), recipient -> {
|
||||
CommunicationActions.composeSmsThroughDefaultApp(this, recipient.getAddress(), getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
inviteButtonView.setVisibility(View.GONE);
|
||||
engageContainerView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearView() {
|
||||
nameView.setText("");
|
||||
numberView.setText("");
|
||||
inviteButtonView.setVisibility(View.GONE);
|
||||
engageContainerView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
public abstract class SimpleTextWatcher implements TextWatcher {
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
onTextChanged(s.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) { }
|
||||
|
||||
public abstract void onTextChanged(String text);
|
||||
}
|
||||
Reference in New Issue
Block a user