Allow editing of contact names.

Took care to properly format CJK names.
This commit is contained in:
Greyson Parrelli 2018-05-10 11:31:38 -07:00
parent 54dbffaf30
commit e6c16cf28d
9 changed files with 449 additions and 11 deletions

View File

@ -407,6 +407,10 @@
android:theme="@style/TextSecure.LightTheme" android:theme="@style/TextSecure.LightTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.ContactNameEditActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.SharedContactDetailsActivity" <activity android:name=".contactshare.SharedContactDetailsActivity"
android:theme="@style/TextSecure.LightNoActionBar" android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/TextSecure.LightActionBar"
android:background="@color/signal_primary"
android:elevation="4dp"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/name_edit_display_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:textSize="20sp"
tools:text="Peter Parker"/>
<org.thoughtcrime.securesms.components.emoji.EmojiEditText
android:id="@+id/name_edit_prefix"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/ContactNameEditActivity_prefix"/>
<org.thoughtcrime.securesms.components.emoji.EmojiEditText
android:id="@+id/name_edit_given_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/ContactNameEditActivity_given_name"/>
<org.thoughtcrime.securesms.components.emoji.EmojiEditText
android:id="@+id/name_edit_middle_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/ContactNameEditActivity_middle_name"/>
<org.thoughtcrime.securesms.components.emoji.EmojiEditText
android:id="@+id/name_edit_family_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/ContactNameEditActivity_family_name"/>
<org.thoughtcrime.securesms.components.emoji.EmojiEditText
android:id="@+id/name_edit_suffix"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/ContactNameEditActivity_suffix"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -28,6 +28,15 @@
android:ellipsize="end" android:ellipsize="end"
tools:text="Peter Parker"/> tools:text="Peter Parker"/>
<ImageButton
android:id="@+id/editable_contact_name_edit_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_create_white_24dp"
android:tint="@color/signal_primary"/>
</LinearLayout> </LinearLayout>
<ImageView <ImageView

View File

@ -97,6 +97,13 @@
<string name="ContactsDatabase_message_s">Message %s</string> <string name="ContactsDatabase_message_s">Message %s</string>
<string name="ContactsDatabase_signal_call_s">Signal Call %s</string> <string name="ContactsDatabase_signal_call_s">Signal Call %s</string>
<!-- ContactNameEditActivity -->
<string name="ContactNameEditActivity_given_name">Given name</string>
<string name="ContactNameEditActivity_family_name">Family name</string>
<string name="ContactNameEditActivity_prefix">Prefix</string>
<string name="ContactNameEditActivity_suffix">Suffix</string>
<string name="ContactNameEditActivity_middle_name">Middle name</string>
<!-- ContactShareEditActivity --> <!-- ContactShareEditActivity -->
<string name="ContactShareEditActivity_type_home">Home</string> <string name="ContactShareEditActivity_type_home">Home</string>
<string name="ContactShareEditActivity_type_mobile">Mobile</string> <string name="ContactShareEditActivity_type_mobile">Mobile</string>

View File

@ -0,0 +1,156 @@
package org.thoughtcrime.securesms.contactshare;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.widget.Toolbar;
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.TextView;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.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
void onTextChanged(String text) {
viewModel.updateGivenName(text);
}
});
familyName.addTextChangedListener(new SimpleTextWatcher() {
@Override
void onTextChanged(String text) {
viewModel.updateFamilyName(text);
}
});
middleName.addTextChangedListener(new SimpleTextWatcher() {
@Override
void onTextChanged(String text) {
viewModel.updateMiddleName(text);
}
});
prefix.addTextChangedListener(new SimpleTextWatcher() {
@Override
void onTextChanged(String text) {
viewModel.updatePrefix(text);
}
});
suffix.addTextChangedListener(new SimpleTextWatcher() {
@Override
void onTextChanged(String text) {
viewModel.updateSuffix(text);
}
});
}
private static 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) { }
abstract void onTextChanged(String text);
}
}

View File

@ -0,0 +1,138 @@
package org.thoughtcrime.securesms.contactshare;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.ViewModel;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.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);
boolean isCJKV = 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);
if (Build.VERSION.SDK_INT >= 19) {
isCJKV |= Character.isIdeographic(codepoint);
}
return isCJKV;
}
}

View File

@ -25,12 +25,14 @@ import org.thoughtcrime.securesms.util.DynamicTheme;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.thoughtcrime.securesms.contactshare.Contact.*;
import static org.thoughtcrime.securesms.contactshare.ContactShareEditViewModel.*; import static org.thoughtcrime.securesms.contactshare.ContactShareEditViewModel.*;
public class ContactShareEditActivity extends PassphraseRequiredActionBarActivity { public class ContactShareEditActivity extends PassphraseRequiredActionBarActivity implements ContactShareEditAdapter.EventListener {
public static final String KEY_CONTACTS = "contacts"; public static final String KEY_CONTACTS = "contacts";
private static final String KEY_CONTACT_IDS = "ids"; private static final String KEY_CONTACT_IDS = "ids";
private static final int CODE_NAME_EDIT = 55;
private final DynamicTheme dynamicTheme = new DynamicTheme(); private final DynamicTheme dynamicTheme = new DynamicTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@ -73,7 +75,7 @@ public class ContactShareEditActivity extends PassphraseRequiredActionBarActivit
contactList.setLayoutManager(new LinearLayoutManager(this)); contactList.setLayoutManager(new LinearLayoutManager(this));
contactList.getLayoutManager().setAutoMeasureEnabled(true); contactList.getLayoutManager().setAutoMeasureEnabled(true);
ContactShareEditAdapter contactAdapter = new ContactShareEditAdapter(GlideApp.with(this), dynamicLanguage.getCurrentLocale()); ContactShareEditAdapter contactAdapter = new ContactShareEditAdapter(GlideApp.with(this), dynamicLanguage.getCurrentLocale(), this);
contactList.setAdapter(contactAdapter); contactList.setAdapter(contactAdapter);
ContactRepository contactRepository = new ContactRepository(this, ContactRepository contactRepository = new ContactRepository(this,
@ -117,4 +119,25 @@ public class ContactShareEditActivity extends PassphraseRequiredActionBarActivit
finish(); 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);
}
}
} }

View File

@ -20,15 +20,19 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import static org.thoughtcrime.securesms.contactshare.Contact.*;
public class ContactShareEditAdapter extends RecyclerView.Adapter<ContactShareEditAdapter.ContactEditViewHolder> { public class ContactShareEditAdapter extends RecyclerView.Adapter<ContactShareEditAdapter.ContactEditViewHolder> {
private final Locale locale;
private final GlideRequests glideRequests; private final GlideRequests glideRequests;
private final Locale locale;
private final EventListener eventListener;
private final List<Contact> contacts; private final List<Contact> contacts;
ContactShareEditAdapter(@NonNull GlideRequests glideRequests, @NonNull Locale locale) { ContactShareEditAdapter(@NonNull GlideRequests glideRequests, @NonNull Locale locale, @NonNull EventListener eventListener) {
this.locale = locale;
this.glideRequests = glideRequests; this.glideRequests = glideRequests;
this.locale = locale;
this.eventListener = eventListener;
this.contacts = new ArrayList<>(); this.contacts = new ArrayList<>();
} }
@ -39,7 +43,7 @@ public class ContactShareEditAdapter extends RecyclerView.Adapter<ContactShareEd
@Override @Override
public void onBindViewHolder(ContactEditViewHolder holder, int position) { public void onBindViewHolder(ContactEditViewHolder holder, int position) {
holder.bind(contacts.get(position), glideRequests); holder.bind(position, contacts.get(position), glideRequests, eventListener);
} }
@Override @Override
@ -61,6 +65,7 @@ public class ContactShareEditAdapter extends RecyclerView.Adapter<ContactShareEd
private final AvatarImageView avatar; private final AvatarImageView avatar;
private final TextView name; private final TextView name;
private final View nameEditButton;
private final ContactFieldAdapter fieldAdapter; private final ContactFieldAdapter fieldAdapter;
ContactEditViewHolder(View itemView, @NonNull Locale locale) { ContactEditViewHolder(View itemView, @NonNull Locale locale) {
@ -68,6 +73,7 @@ public class ContactShareEditAdapter extends RecyclerView.Adapter<ContactShareEd
this.avatar = itemView.findViewById(R.id.editable_contact_avatar); this.avatar = itemView.findViewById(R.id.editable_contact_avatar);
this.name = itemView.findViewById(R.id.editable_contact_name); 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, true); this.fieldAdapter = new ContactFieldAdapter(locale, true);
RecyclerView fields = itemView.findViewById(R.id.editable_contact_fields); RecyclerView fields = itemView.findViewById(R.id.editable_contact_fields);
@ -76,7 +82,7 @@ public class ContactShareEditAdapter extends RecyclerView.Adapter<ContactShareEd
fields.setAdapter(fieldAdapter); fields.setAdapter(fieldAdapter);
} }
void bind(@NonNull Contact contact, @NonNull GlideRequests glideRequests) { void bind(int position, @NonNull Contact contact, @NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
Context context = itemView.getContext(); Context context = itemView.getContext();
if (contact.getAvatarAttachment() != null && contact.getAvatarAttachment().getDataUri() != null) { if (contact.getAvatarAttachment() != null && contact.getAvatarAttachment().getDataUri() != null) {
@ -93,7 +99,12 @@ public class ContactShareEditAdapter extends RecyclerView.Adapter<ContactShareEd
} }
name.setText(ContactUtil.getDisplayName(contact)); name.setText(ContactUtil.getDisplayName(contact));
nameEditButton.setOnClickListener(v -> eventListener.onNameEditClicked(position, contact.getName()));
fieldAdapter.setFields(context, contact.getPhoneNumbers(), contact.getEmails(), contact.getPostalAddresses()); fieldAdapter.setFields(context, contact.getPhoneNumbers(), contact.getEmails(), contact.getPostalAddresses());
} }
} }
interface EventListener {
void onNameEditClicked(int position, @NonNull Name name);
}
} }

View File

@ -8,6 +8,7 @@ import android.support.annotation.NonNull;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.contactshare.Contact.Name;
import org.thoughtcrime.securesms.util.SingleLiveEvent; import org.thoughtcrime.securesms.util.SingleLiveEvent;
import java.util.ArrayList; import java.util.ArrayList;
@ -60,6 +61,25 @@ class ContactShareEditViewModel extends ViewModel {
return events; 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) { private <E extends Selectable> List<E> trimSelectables(List<E> selectables) {
return Stream.of(selectables).filter(Selectable::isSelected).toList(); return Stream.of(selectables).filter(Selectable::isSelected).toList();
} }