From e6c16cf28d0fd3d6a19347529b2e39d03f5d279d Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 10 May 2018 11:31:38 -0700 Subject: [PATCH] Allow editing of contact names. Took care to properly format CJK names. --- AndroidManifest.xml | 4 + res/layout/activity_contact_name_edit.xml | 70 ++++++++ res/layout/item_editable_contact.xml | 9 + res/values/strings.xml | 7 + .../contactshare/ContactNameEditActivity.java | 156 ++++++++++++++++++ .../ContactNameEditViewModel.java | 138 ++++++++++++++++ .../ContactShareEditActivity.java | 27 ++- .../contactshare/ContactShareEditAdapter.java | 29 +++- .../ContactShareEditViewModel.java | 20 +++ 9 files changed, 449 insertions(+), 11 deletions(-) create mode 100644 res/layout/activity_contact_name_edit.xml create mode 100644 src/org/thoughtcrime/securesms/contactshare/ContactNameEditActivity.java create mode 100644 src/org/thoughtcrime/securesms/contactshare/ContactNameEditViewModel.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 1f91738484..cf5c485ec6 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -407,6 +407,10 @@ android:theme="@style/TextSecure.LightTheme" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> + + diff --git a/res/layout/activity_contact_name_edit.xml b/res/layout/activity_contact_name_edit.xml new file mode 100644 index 0000000000..1a4ad06c35 --- /dev/null +++ b/res/layout/activity_contact_name_edit.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/item_editable_contact.xml b/res/layout/item_editable_contact.xml index b274a0ff77..27e1eca4d8 100644 --- a/res/layout/item_editable_contact.xml +++ b/res/layout/item_editable_contact.xml @@ -28,6 +28,15 @@ android:ellipsize="end" tools:text="Peter Parker"/> + + Message %s Signal Call %s + + Given name + Family name + Prefix + Suffix + Middle name + Home Mobile diff --git a/src/org/thoughtcrime/securesms/contactshare/ContactNameEditActivity.java b/src/org/thoughtcrime/securesms/contactshare/ContactNameEditActivity.java new file mode 100644 index 0000000000..d744894e68 --- /dev/null +++ b/src/org/thoughtcrime/securesms/contactshare/ContactNameEditActivity.java @@ -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); + } +} diff --git a/src/org/thoughtcrime/securesms/contactshare/ContactNameEditViewModel.java b/src/org/thoughtcrime/securesms/contactshare/ContactNameEditViewModel.java new file mode 100644 index 0000000000..a453001f53 --- /dev/null +++ b/src/org/thoughtcrime/securesms/contactshare/ContactNameEditViewModel.java @@ -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 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 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; + } +} diff --git a/src/org/thoughtcrime/securesms/contactshare/ContactShareEditActivity.java b/src/org/thoughtcrime/securesms/contactshare/ContactShareEditActivity.java index efc0962a5f..4f6a932b6a 100644 --- a/src/org/thoughtcrime/securesms/contactshare/ContactShareEditActivity.java +++ b/src/org/thoughtcrime/securesms/contactshare/ContactShareEditActivity.java @@ -25,12 +25,14 @@ 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 { +public class ContactShareEditActivity extends PassphraseRequiredActionBarActivity implements ContactShareEditAdapter.EventListener { public static final String KEY_CONTACTS = "contacts"; private static final String KEY_CONTACT_IDS = "ids"; + private static final int CODE_NAME_EDIT = 55; private final DynamicTheme dynamicTheme = new DynamicTheme(); private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); @@ -73,7 +75,7 @@ public class ContactShareEditActivity extends PassphraseRequiredActionBarActivit contactList.setLayoutManager(new LinearLayoutManager(this)); 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); ContactRepository contactRepository = new ContactRepository(this, @@ -117,4 +119,25 @@ public class ContactShareEditActivity extends PassphraseRequiredActionBarActivit 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); + } + } } diff --git a/src/org/thoughtcrime/securesms/contactshare/ContactShareEditAdapter.java b/src/org/thoughtcrime/securesms/contactshare/ContactShareEditAdapter.java index 9ddf07e902..34f48b7d38 100644 --- a/src/org/thoughtcrime/securesms/contactshare/ContactShareEditAdapter.java +++ b/src/org/thoughtcrime/securesms/contactshare/ContactShareEditAdapter.java @@ -20,15 +20,19 @@ 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 { - private final Locale locale; private final GlideRequests glideRequests; + private final Locale locale; + private final EventListener eventListener; private final List contacts; - ContactShareEditAdapter(@NonNull GlideRequests glideRequests, @NonNull Locale locale) { - this.locale = locale; + ContactShareEditAdapter(@NonNull GlideRequests glideRequests, @NonNull Locale locale, @NonNull EventListener eventListener) { this.glideRequests = glideRequests; + this.locale = locale; + this.eventListener = eventListener; this.contacts = new ArrayList<>(); } @@ -39,7 +43,7 @@ public class ContactShareEditAdapter extends RecyclerView.Adapter eventListener.onNameEditClicked(position, contact.getName())); + fieldAdapter.setFields(context, contact.getPhoneNumbers(), contact.getEmails(), contact.getPostalAddresses()); } } + + interface EventListener { + void onNameEditClicked(int position, @NonNull Name name); + } } diff --git a/src/org/thoughtcrime/securesms/contactshare/ContactShareEditViewModel.java b/src/org/thoughtcrime/securesms/contactshare/ContactShareEditViewModel.java index 6692aab51f..1c4caa884b 100644 --- a/src/org/thoughtcrime/securesms/contactshare/ContactShareEditViewModel.java +++ b/src/org/thoughtcrime/securesms/contactshare/ContactShareEditViewModel.java @@ -8,6 +8,7 @@ import android.support.annotation.NonNull; import com.annimon.stream.Stream; +import org.thoughtcrime.securesms.contactshare.Contact.Name; import org.thoughtcrime.securesms.util.SingleLiveEvent; import java.util.ArrayList; @@ -60,6 +61,25 @@ class ContactShareEditViewModel extends ViewModel { return events; } + void updateContactName(int contactPosition, @NonNull Name name) { + if (name.isEmpty()) { + events.postValue(Event.BAD_CONTACT); + return; + } + + List 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 List trimSelectables(List selectables) { return Stream.of(selectables).filter(Selectable::isSelected).toList(); }