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