blob: e68cf29f881e1b6be0434db28cf418139afc5c91 [file] [log] [blame]
package org.wordpress.android.ui.people;
import android.app.Fragment;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import org.wordpress.android.R;
import org.wordpress.android.WordPress;
import org.wordpress.android.models.Blog;
import org.wordpress.android.models.Role;
import org.wordpress.android.ui.people.utils.PeopleUtils;
import org.wordpress.android.ui.people.utils.PeopleUtils.ValidateUsernameCallback.ValidationResult;
import org.wordpress.android.util.EditTextUtils;
import org.wordpress.android.util.NetworkUtils;
import org.wordpress.android.util.StringUtils;
import org.wordpress.android.util.ToastUtils;
import org.wordpress.android.widgets.MultiUsernameEditText;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class PeopleInviteFragment extends Fragment implements
RoleSelectDialogFragment.OnRoleSelectListener,
PeopleManagementActivity.InvitationSender {
private static final String FLAG_SUCCESS = "SUCCESS";
private static final String ARG_BLOGID = "ARG_BLOGID";
private static final int MAX_NUMBER_OF_INVITEES = 10;
private static final String[] USERNAME_DELIMITERS = {" ", ","};
private ViewGroup mUsernamesContainer;
private MultiUsernameEditText mUsernameEditText;
private TextView mRoleTextView;
private EditText mCustomMessageEditText;
private final Map<String, ViewGroup> mUsernameButtons = new LinkedHashMap<>();
private final HashMap<String, String> mUsernameResults = new HashMap<>();
private final Map<String, TextView> mUsernameErrorViews = new Hashtable<>();
private Role mRole;
private String mCustomMessage = "";
private boolean mInviteOperationInProgress = false;
public static PeopleInviteFragment newInstance(String dotComBlogId) {
PeopleInviteFragment peopleInviteFragment = new PeopleInviteFragment();
Bundle bundle = new Bundle();
bundle.putString(ARG_BLOGID, dotComBlogId);
peopleInviteFragment.setArguments(bundle);
return peopleInviteFragment;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.people_invite, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
menu.getItem(0).setEnabled(!mInviteOperationInProgress); // here pass the index of send menu item
super.onPrepareOptionsMenu(menu);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment across configuration changes
// WARNING: use setRetainInstance wisely. In this case we need this to be able to get the
// results of network connections in the same fragment if going through a configuration change
// (for example, device rotation occurs). Given the simplicity of this particular use case
// (the fragment state keeps only a couple of EditText components and the SAVE button, it is
// OK to use it here.
setRetainInstance(true);
}
@Override
public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
setHasOptionsMenu(true);
return inflater.inflate(R.layout.people_invite_fragment, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mUsernamesContainer = (ViewGroup) view.findViewById(R.id.usernames);
mUsernamesContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
EditTextUtils.showSoftInput(mUsernameEditText);
}
});
Role role = mRole;
if (role == null) {
role = getDefaultRole();
}
mUsernameEditText = (MultiUsernameEditText) view.findViewById(R.id.invite_usernames);
//handle key preses from hardware keyboard
mUsernameEditText.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View view, int i, KeyEvent keyEvent) {
return keyEvent.getKeyCode() == KeyEvent.KEYCODE_DEL
&& keyEvent.getAction() == KeyEvent.ACTION_DOWN
&& removeLastEnteredUsername();
}
});
mUsernameEditText.setOnBackspacePressedListener(new MultiUsernameEditText.OnBackspacePressedListener() {
@Override
public boolean onBackspacePressed() {
return removeLastEnteredUsername();
}
});
mUsernameEditText.addTextChangedListener(new TextWatcher() {
private boolean shouldIgnoreChanges = false;
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (shouldIgnoreChanges) { //used to avoid double call after calling setText from this method
return;
}
shouldIgnoreChanges = true;
if (mUsernameButtons.size() >= MAX_NUMBER_OF_INVITEES && !TextUtils.isEmpty(s)) {
resetEditTextContent(mUsernameEditText);
} else if (endsWithDelimiter(mUsernameEditText.getText().toString())) {
addUsername(mUsernameEditText, null);
}
shouldIgnoreChanges = false;
}
@Override
public void afterTextChanged(Editable s) {
}
});
mUsernameEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE || (event != null && event.getKeyCode() == KeyEvent
.KEYCODE_ENTER)) {
addUsername(mUsernameEditText, null);
return true;
} else {
return false;
}
}
});
mUsernameEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus && mUsernameEditText.getText().toString().length() > 0) {
addUsername(mUsernameEditText, null);
}
}
});
if (mUsernameButtons.size() > 0) {
ArrayList<String> usernames = new ArrayList<>(mUsernameButtons.keySet());
populateUsernameButtons(usernames);
}
View roleContainer = view.findViewById(R.id.role_container);
roleContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
RoleSelectDialogFragment.show(PeopleInviteFragment.this, 0, isPrivateSite());
}
});
mRoleTextView = (TextView) view.findViewById(R.id.role);
setRole(role);
final int MAX_CHARS = getResources().getInteger(R.integer.invite_message_char_limit);
final TextView remainingCharsTextView = (TextView) view.findViewById(R.id.message_remaining);
mCustomMessageEditText = (EditText) view.findViewById(R.id.message);
mCustomMessageEditText.addTextChangedListener(new 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) {
mCustomMessage = mCustomMessageEditText.getText().toString();
updateRemainingCharsView(remainingCharsTextView, mCustomMessage, MAX_CHARS);
}
@Override
public void afterTextChanged(Editable s) {
}
});
updateRemainingCharsView(remainingCharsTextView, mCustomMessage, MAX_CHARS);
}
private boolean endsWithDelimiter(String string) {
if (TextUtils.isEmpty(string)) {
return false;
}
for (String usernameDelimiter : USERNAME_DELIMITERS) {
if (string.endsWith(usernameDelimiter)) {
return true;
}
}
return false;
}
private String removeDelimiterFromUsername(String username) {
if (TextUtils.isEmpty(username)) {
return username;
}
String trimmedUsername = username.trim();
for (String usernameDelimiter : USERNAME_DELIMITERS) {
if (trimmedUsername.endsWith(usernameDelimiter)) {
return trimmedUsername.substring(0, trimmedUsername.length() - usernameDelimiter.length());
}
}
return trimmedUsername;
}
private void resetEditTextContent(EditText editText) {
if (editText != null) {
editText.setText("");
}
}
private Role getDefaultRole() {
Role[] inviteRoles = Role.inviteRoles(isPrivateSite());
return inviteRoles[0];
}
private void updateRemainingCharsView(TextView remainingCharsTextView, String currentString, int limit) {
remainingCharsTextView.setText(StringUtils.getQuantityString(getActivity(),
R.string.invite_message_remaining_zero,
R.string.invite_message_remaining_one,
R.string.invite_message_remaining_other, limit - (currentString == null ? 0 : currentString.length())));
}
private void populateUsernameButtons(Collection<String> usernames) {
if (usernames != null && usernames.size() > 0) {
for (String username : usernames) {
mUsernameButtons.put(username, buttonizeUsername(username));
}
validateAndStyleUsername(usernames, null);
}
}
private ViewGroup buttonizeUsername(final String username) {
if (!isAdded()) {
return null;
}
final ViewGroup usernameButton = (ViewGroup) LayoutInflater.from(getActivity()).inflate(R.layout
.invite_username_button, null);
final TextView usernameTextView = (TextView) usernameButton.findViewById(R.id.username);
usernameTextView.setText(username);
mUsernamesContainer.addView(usernameButton, mUsernamesContainer.getChildCount() - 1);
final ImageButton delete = (ImageButton) usernameButton.findViewById(R.id.username_delete);
delete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
removeUsername(username);
}
});
return usernameButton;
}
private void addUsername(EditText editText, ValidationEndListener validationEndListener) {
String username = removeDelimiterFromUsername(editText.getText().toString());
resetEditTextContent(editText);
if (username.isEmpty() || mUsernameButtons.keySet().contains(username)) {
if (validationEndListener != null) {
validationEndListener.onValidationEnd();
}
return;
}
final ViewGroup usernameButton = buttonizeUsername(username);
mUsernameButtons.put(username, usernameButton);
validateAndStyleUsername(Collections.singletonList(username), validationEndListener);
}
private void removeUsername(String username) {
final ViewGroup usernamesView = (ViewGroup) getView().findViewById(R.id.usernames);
ViewGroup removedButton = mUsernameButtons.remove(username);
mUsernameResults.remove(username);
usernamesView.removeView(removedButton);
updateUsernameError(username, null);
}
private boolean isUserInInvitees(String username) {
return mUsernameButtons.get(username) != null;
}
/**
* Deletes the last entered username.
* @return true if the username was deleted
*/
private boolean removeLastEnteredUsername() {
if (!TextUtils.isEmpty(mUsernameEditText.getText())) {
return false;
}
//try and remove the last entered username
List<String> list = new ArrayList<>(mUsernameButtons.keySet());
if (!list.isEmpty()) {
String username = list.get(list.size() - 1);
removeUsername(username);
return true;
}
return false;
}
@Override
public void onRoleSelected(Role newRole) {
setRole(newRole);
if (!mUsernameButtons.keySet().isEmpty()) {
// clear the username results list and let the 'validate' routine do the updates
mUsernameResults.clear();
validateAndStyleUsername(mUsernameButtons.keySet(), null);
}
}
private void setRole(Role newRole) {
mRole = newRole;
mRoleTextView.setText(newRole.toDisplayString());
}
private void validateAndStyleUsername(Collection<String> usernames, final ValidationEndListener validationEndListener) {
List<String> usernamesToCheck = new ArrayList<>();
for (String username : usernames) {
if (mUsernameResults.containsKey(username)) {
String resultMessage = mUsernameResults.get(username);
styleButton(username, resultMessage);
updateUsernameError(username, resultMessage);
} else {
styleButton(username, null);
updateUsernameError(username, null);
usernamesToCheck.add(username);
}
}
if (usernamesToCheck.size() > 0) {
String dotComBlogId = getArguments().getString(ARG_BLOGID);
PeopleUtils.validateUsernames(usernamesToCheck, mRole, dotComBlogId, new PeopleUtils.ValidateUsernameCallback() {
@Override
public void onUsernameValidation(String username, ValidationResult validationResult) {
if (!isAdded()) {
return;
}
if(!isUserInInvitees(username)){
//user is removed from invitees before validation
return;
}
final String usernameResultString = getValidationErrorString(username, validationResult);
mUsernameResults.put(username, usernameResultString);
styleButton(username, usernameResultString);
updateUsernameError(username, usernameResultString);
}
@Override
public void onValidationFinished() {
if (validationEndListener != null) {
validationEndListener.onValidationEnd();
}
}
@Override
public void onError() {
// properly style the button
}
});
} else {
if (validationEndListener != null) {
validationEndListener.onValidationEnd();
}
}
}
public interface ValidationEndListener {
void onValidationEnd();
}
private void styleButton(String username, @Nullable String validationResultMessage) {
if (!isAdded()) {
return;
}
TextView textView = (TextView) mUsernameButtons.get(username).findViewById(R.id.username);
textView.setTextColor(ContextCompat.getColor(getActivity(),
validationResultMessage == null ? R.color.grey_dark :
(validationResultMessage.equals(FLAG_SUCCESS) ? R.color.blue_wordpress : R.color.alert_red)));
}
private
@Nullable
String getValidationErrorString(String username, ValidationResult validationResult) {
switch (validationResult) {
case USER_NOT_FOUND:
return getString(R.string.invite_username_not_found, username);
case ALREADY_MEMBER:
return getString(R.string.invite_already_a_member, username);
case ALREADY_FOLLOWING:
return getString(R.string.invite_already_following, username);
case BLOCKED_INVITES:
return getString(R.string.invite_user_blocked_invites, username);
case INVALID_EMAIL:
return getString(R.string.invite_invalid_email, username);
case USER_FOUND:
return FLAG_SUCCESS;
}
return null;
}
private void updateUsernameError(String username, @Nullable String usernameResult) {
if (!isAdded()) {
return;
}
TextView usernameErrorTextView;
if (mUsernameErrorViews.containsKey(username)) {
usernameErrorTextView = mUsernameErrorViews.get(username);
if (usernameResult == null || usernameResult.equals(FLAG_SUCCESS)) {
// no error so we need to remove the existing error view
((ViewGroup) usernameErrorTextView.getParent()).removeView(usernameErrorTextView);
mUsernameErrorViews.remove(username);
return;
}
} else {
if (usernameResult == null || usernameResult.equals(FLAG_SUCCESS)) {
// no error so no need to create a new error view
return;
}
usernameErrorTextView = (TextView) LayoutInflater.from(getActivity())
.inflate(R.layout.people_invite_error_view, null);
final ViewGroup usernameErrorsContainer = (ViewGroup) getView()
.findViewById(R.id.username_errors_container);
usernameErrorsContainer.addView(usernameErrorTextView);
mUsernameErrorViews.put(username, usernameErrorTextView);
}
usernameErrorTextView.setText(usernameResult);
}
private void clearUsernames(Collection<String> usernames) {
for (String username : usernames) {
removeUsername(username);
}
if (mUsernameButtons.size() == 0) {
setRole(getDefaultRole());
resetEditTextContent(mCustomMessageEditText);
}
}
@Override
public void send() {
if (!isAdded()) {
return;
}
if (!NetworkUtils.checkConnection(getActivity())) {
enableSendButton(true);
return;
}
enableSendButton(false);
if (mUsernameEditText.getText().toString().length() > 0) {
addUsername(mUsernameEditText, new ValidationEndListener() {
@Override
public void onValidationEnd() {
if (!checkAndSend()) {
//re-enable SEND button if validation failed
enableSendButton(true);
}
}
});
} else {
if (!checkAndSend()) {
//re-enable SEND button if validation failed
enableSendButton(true);
}
}
}
/*
* returns true if send is attempted, false if validation failed
* */
private boolean checkAndSend() {
if (!isAdded()) {
return false;
}
if (!NetworkUtils.checkConnection(getActivity())) {
return false;
}
if (mUsernameButtons.size() == 0) {
ToastUtils.showToast(getActivity(), R.string.invite_error_no_usernames);
return false;
}
int invalidCount = 0;
for (String usernameResultString : mUsernameResults.values()) {
if (!usernameResultString.equals(FLAG_SUCCESS)) {
invalidCount++;
}
}
if (invalidCount > 0) {
ToastUtils.showToast(getActivity(), StringUtils.getQuantityString(getActivity(), 0,
R.string.invite_error_invalid_usernames_one,
R.string.invite_error_invalid_usernames_multiple, invalidCount));
return false;
}
//set the "SEND" option disabled
enableSendButton(false);
String dotComBlogId = getArguments().getString(ARG_BLOGID);
PeopleUtils.sendInvitations(new ArrayList<>(mUsernameButtons.keySet()), mRole, mCustomMessage,
dotComBlogId, new PeopleUtils.InvitationsSendCallback() {
@Override
public void onSent(List<String> succeededUsernames, Map<String, String> failedUsernameErrors) {
if (!isAdded()) {
return;
}
clearUsernames(succeededUsernames);
if (failedUsernameErrors.size() != 0) {
clearUsernames(failedUsernameErrors.keySet());
for (Map.Entry<String, String> error : failedUsernameErrors.entrySet()) {
final String username = error.getKey();
final String errorMessage = error.getValue();
mUsernameResults.put(username, getString(R.string.invite_error_for_username,
username, errorMessage));
}
populateUsernameButtons(failedUsernameErrors.keySet());
ToastUtils.showToast(getActivity(), succeededUsernames.isEmpty()
? R.string.invite_error_sending : R.string.invite_error_some_failed);
} else {
ToastUtils.showToast(getActivity(), R.string.invite_sent, ToastUtils.Duration.LONG);
}
//set the "SEND" option enabled again
enableSendButton(true);
}
@Override
public void onError() {
if (!isAdded()) {
return;
}
ToastUtils.showToast(getActivity(), R.string.invite_error_sending);
//set the "SEND" option enabled again
enableSendButton(true);
}
});
return true;
}
private void enableSendButton(boolean enable) {
mInviteOperationInProgress = !enable;
if (getActivity() != null) {
getActivity().invalidateOptionsMenu();
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
//we need to remove focus listener when view is destroyed (ex. orientation change) to prevent mUsernameEditText
//content from being converted to username
if (mUsernameEditText != null) {
mUsernameEditText.setOnFocusChangeListener(null);
}
}
private boolean isPrivateSite() {
String dotComBlogId = getArguments().getString(ARG_BLOGID);
Blog blog = WordPress.wpDB.getBlogForDotComBlogId(dotComBlogId);
return blog != null && blog.isPrivate();
}
}