Skip to content
Snippets Groups Projects
Commit 23da6b7c authored by Mohamed Amine Younes Bouacida's avatar Mohamed Amine Younes Bouacida
Browse files

account creation: clarify availability state

+ change the fragment, layout and presenter in order to add this new feature
+ take  care of any errors in the daemon response (e.g : network disruption)
+ handle the cancel and proceed events properly
+ fix error display regarding password errors

Change-Id: Ie9b11502391108dd1a62b0a5460bff18a8615d12
Gitlab: #628
parent 6f87dbb4
No related branches found
No related tags found
No related merge requests found
......@@ -22,7 +22,6 @@ package cx.ring.account;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import com.google.android.material.textfield.TextInputLayout;
import android.text.Editable;
import android.text.InputFilter;
import android.view.View;
......@@ -31,9 +30,14 @@ import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.Switch;
import androidx.annotation.NonNull;
import com.google.android.material.textfield.TextInputLayout;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnCheckedChanged;
......@@ -42,11 +46,12 @@ import butterknife.OnEditorAction;
import butterknife.OnTextChanged;
import cx.ring.R;
import cx.ring.dependencyinjection.RingInjectionComponent;
import cx.ring.mvp.BaseSupportFragment;
import cx.ring.mvp.AccountCreationModel;
import cx.ring.mvp.BaseSupportFragment;
import cx.ring.utils.RegisteredNameFilter;
public class RingAccountCreationFragment extends BaseSupportFragment<RingAccountCreationPresenter> implements RingAccountCreationView {
public class RingAccountCreationFragment extends BaseSupportFragment<RingAccountCreationPresenter>
implements RingAccountCreationView {
@BindView(R.id.switch_ring_username)
protected Switch mUsernameSwitch;
......@@ -78,6 +83,12 @@ public class RingAccountCreationFragment extends BaseSupportFragment<RingAccount
@BindView(R.id.create_account)
protected Button mCreateAccountButton;
@BindView(R.id.ring_username_availability_image_view)
protected ImageView mUsernameAvailabilityImageView;
@BindView(R.id.ring_username_availability_spinner)
protected ProgressBar mUsernameAvailabilitySpinner;
private AccountCreationModel model;
public static RingAccountCreationFragment newInstance(AccountCreationModelImpl ringAccountViewModel) {
......@@ -111,7 +122,8 @@ public class RingAccountCreationFragment extends BaseSupportFragment<RingAccount
super.onResume();
if (mUsernameBox.getVisibility() == View.VISIBLE) {
mUsernameTxt.requestFocus();
InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
InputMethodManager imm = (InputMethodManager) requireActivity().
getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mUsernameTxt, InputMethodManager.SHOW_IMPLICIT);
}
}
......@@ -136,11 +148,6 @@ public class RingAccountCreationFragment extends BaseSupportFragment<RingAccount
presenter.createAccount();
}
@OnTextChanged(value = R.id.ring_username, callback = OnTextChanged.Callback.TEXT_CHANGED)
public void onUsernameChanged() {
mUsernameTxt.setError(null);
}
@OnTextChanged(value = R.id.ring_password, callback = OnTextChanged.Callback.TEXT_CHANGED)
public void afterPasswordChanged(Editable txt) {
presenter.passwordChanged(txt.toString());
......@@ -165,27 +172,46 @@ public class RingAccountCreationFragment extends BaseSupportFragment<RingAccount
}
@Override
public void enableTextError() {
public void updateUsernameAvailability(UsernameAvailabilityStatus status) {
mUsernameAvailabilitySpinner.setVisibility(View.GONE);
mUsernameAvailabilityImageView.setVisibility(View.VISIBLE);
switch (status){
case ERROR:
mUsernameTxtBox.setErrorEnabled(true);
mUsernameTxtBox.setError(getString(R.string.looking_for_username_availability));
}
@Override
public void disableTextError() {
mUsernameTxtBox.setErrorEnabled(false);
mUsernameTxtBox.setError(null);
}
@Override
public void showExistingNameError() {
mUsernameTxtBox.setError(getString(R.string.unknown_error));
mUsernameAvailabilityImageView.setImageDrawable(getResources().
getDrawable(R.drawable.ic_error_red));
break;
case ERROR_USERNAME_INVALID:
mUsernameTxtBox.setErrorEnabled(true);
mUsernameTxtBox.setError(getString(R.string.invalid_username));
mUsernameAvailabilityImageView.setImageDrawable(getResources().
getDrawable(R.drawable.ic_error_red));
break;
case ERROR_USERNAME_TAKEN:
mUsernameTxtBox.setErrorEnabled(true);
mUsernameTxtBox.setError(getString(R.string.username_already_taken));
mUsernameAvailabilityImageView.setImageDrawable(getResources().
getDrawable(R.drawable.ic_error_red));
break;
case LOADING:
mUsernameTxtBox.setErrorEnabled(false);
mUsernameAvailabilityImageView.setVisibility(View.INVISIBLE);
mUsernameAvailabilitySpinner.setVisibility(View.VISIBLE);
break;
case AVAILABLE:
mUsernameTxtBox.setErrorEnabled(false);
mUsernameAvailabilityImageView.setImageDrawable(getResources().
getDrawable(R.drawable.ic_good_green));
break;
case RESET:
mUsernameTxtBox.setErrorEnabled(false);
mUsernameAvailabilityImageView.setVisibility(View.INVISIBLE);
enableNextButton(false);
default:
mUsernameAvailabilityImageView.setVisibility(View.INVISIBLE);
break;
}
@Override
public void showInvalidNameError() {
mUsernameTxtBox.setErrorEnabled(true);
mUsernameTxtBox.setError(getString(R.string.invalid_username));
}
@Override
......
......@@ -72,6 +72,18 @@ public abstract class RingGuidedStepFragment<T extends RootPresenter> extends Gu
.icon(icon)
.build());
}
protected static void addDisabledNonFocusableAction(Context context, List<GuidedAction> actions, long id, String title, String desc, Drawable icon) {
actions.add(new GuidedAction.Builder(context)
.id(id)
.title(title)
.description(desc)
.enabled(false)
.focusable(false)
.icon(icon)
.build());
}
protected static void addDisabledAction(Context context, List<GuidedAction> actions, long id, String title, String desc, Drawable icon,boolean next) {
actions.add(new GuidedAction.Builder(context)
.id(id)
......
......@@ -62,11 +62,15 @@ public class TVRingAccountCreationFragment
@Override
public void afterTextChanged(Editable s) {
String newName = s.toString();
if (!newName.equals(getResources().getString(R.string.register_username))) {
boolean empty = newName.isEmpty();
/** If the username is empty make sure to set isRegisterUsernameChecked
* to False, this allows to create an account with an empty username */
presenter.ringCheckChanged(!empty);
if (!empty)
/** Send the newName even when empty (in order to reset the views) */
presenter.userNameChanged(newName);
}
}
};
public TVRingAccountCreationFragment() {
......@@ -108,7 +112,7 @@ public class TVRingAccountCreationFragment
@Override
public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
addEditTextAction(getActivity(), actions, USERNAME, R.string.register_username, R.string.prompt_new_username);
addDisabledAction(getActivity(), actions, CHECK, "", "", null);
addDisabledNonFocusableAction(getActivity(), actions, CHECK, "", "", null);
addPasswordAction(getActivity(), actions, PASSWORD, getString(R.string.prompt_new_password_optional), getString(R.string.enter_password), "");
addPasswordAction(getActivity(), actions, PASSWORD_CONFIRMATION, getString(R.string.prompt_new_password_repeat), getString(R.string.enter_password), "");
addDisabledAction(getActivity(), actions, CONTINUE, getString(R.string.action_create), "", null, true);
......@@ -128,28 +132,43 @@ public class TVRingAccountCreationFragment
@Override
public long onGuidedActionEditedAndProceed(GuidedAction action) {
if (action.getId() == PASSWORD) {
onGuidedActionChange(action);
return GuidedAction.ACTION_ID_NEXT;
}
@Override
public void onGuidedActionEditCanceled(GuidedAction action) {
onGuidedActionChange(action);
}
private void onGuidedActionChange(GuidedAction action){
switch ((int) action.getId()){
case USERNAME:
usernameChanged(action);
break;
case PASSWORD:
passwordChanged(action);
} else if (action.getId() == PASSWORD_CONFIRMATION) {
break;
case PASSWORD_CONFIRMATION:
confirmPasswordChanged(action);
} else if (action.getId() == USERNAME) {
break;
}
}
private void usernameChanged(GuidedAction action) {
String username = action.getEditTitle().toString();
ViewGroup view = (ViewGroup) getActionItemView(findActionPositionById(USERNAME));
if (view != null) {
EditText text = view.findViewById(R.id.guidedactions_item_title);
text.removeTextChangedListener(mUsernameWatcher);
}
String username = action.getEditTitle().toString();
boolean empty = username.isEmpty();
if (empty) {
if(username.isEmpty())
action.setTitle(getString(R.string.register_username));
} else {
else
action.setTitle(username);
}
GuidedAction a = findActionById(CHECK);
a.setEnabled(!empty);
notifyActionChanged(findActionPositionById(CHECK));
}
return GuidedAction.ACTION_ID_NEXT;
notifyActionChanged(findActionPositionById(PASSWORD));
}
private void passwordChanged(GuidedAction action) {
......@@ -180,59 +199,66 @@ public class TVRingAccountCreationFragment
}
@Override
public void enableTextError() {
GuidedAction action = findActionById(CHECK);
action.setIcon(null);
action.setTitle(getString(R.string.looking_for_username_availability));
notifyActionChanged(findActionPositionById(CHECK));
}
@Override
public void disableTextError() {
GuidedAction action = findActionById(CHECK);
action.setIcon(null);
action.setDescription("");
notifyActionChanged(findActionPositionById(CHECK));
}
@Override
public void showExistingNameError() {
GuidedAction action = findActionById(CHECK);
action.setIcon(getResources().getDrawable(R.drawable.ic_error_red));
action.setDescription(getString(R.string.username_already_taken));
notifyActionChanged(findActionPositionById(CHECK));
public void updateUsernameAvailability(UsernameAvailabilityStatus status) {
GuidedAction actionCheck = findActionById(CHECK);
switch (status){
case ERROR:
actionCheck.setTitle(getResources().getString(R.string.generic_error));
displayErrorIconTitle(actionCheck, getString(R.string.unknown_error));
break;
case ERROR_USERNAME_INVALID:
displayErrorIconTitle(actionCheck,getString(R.string.invalid_username));
break;
case ERROR_USERNAME_TAKEN:
displayErrorIconTitle(actionCheck,
getString(R.string.username_already_taken));
break;
case LOADING:
actionCheck.setIcon(null);
actionCheck.setTitle(getResources().
getString(R.string.looking_for_username_availability));
break;
case AVAILABLE:
actionCheck.setTitle(getString(R.string.username_available));
actionCheck.setIcon(getResources().getDrawable(R.drawable.ic_good_green));
break;
case RESET:
actionCheck.setIcon(null);
actionCheck.setTitle("");
enableNextButton(false);
default:
actionCheck.setIcon(null);
break;
}
@Override
public void showInvalidNameError() {
GuidedAction action = findActionById(CHECK);
action.setIcon(getResources().getDrawable(R.drawable.ic_error_red));
action.setDescription(getString(R.string.invalid_username));
notifyActionChanged(findActionPositionById(CHECK));
}
@Override
public void showInvalidPasswordError(boolean display) {
if (display) {
GuidedAction action = findActionById(CONTINUE);
action.setIcon(getResources().getDrawable(R.drawable.ic_error_red));
action.setDescription(getString(R.string.error_password_char_count));
if (display) {
displayErrorIconDescription(action,getString(R.string.error_password_char_count));
action.setEnabled(false);
} else {
action.setDescription("");
}
notifyActionChanged(findActionPositionById(CONTINUE));
}
@Override
public void showNonMatchingPasswordError(boolean display) {
if (display) {
GuidedAction action = findActionById(CONTINUE);
action.setIcon(getResources().getDrawable(R.drawable.ic_error_red));
action.setDescription(getString(R.string.error_passwords_not_equals));
if (display) {
displayErrorIconDescription(action,getString(R.string.error_passwords_not_equals));
action.setEnabled(false);
} else {
action.setDescription("");
}
notifyActionChanged(findActionPositionById(CONTINUE));
}
@Override
public void displayUsernameBox(boolean display) {
}
......@@ -240,17 +266,10 @@ public class TVRingAccountCreationFragment
@Override
public void enableNextButton(boolean enabled) {
Log.d(TAG, "enableNextButton: " + enabled);
GuidedAction actionCheck = findActionById(CHECK);
GuidedAction actionContinue = findActionById(CONTINUE);
if (enabled) {
actionCheck.setIcon(getResources().getDrawable(R.drawable.ic_good_green));
actionCheck.setTitle(getString(R.string.no_registered_name_for_account));
actionCheck.setDescription("");
if (enabled)
actionContinue.setIcon(null);
actionCheck.setDescription("");
}
actionContinue.setEnabled(enabled);
notifyActionChanged(findActionPositionById(CHECK));
notifyActionChanged(findActionPositionById(CONTINUE));
}
......@@ -270,4 +289,15 @@ public class TVRingAccountCreationFragment
}
}
private void displayErrorIconTitle(GuidedAction action, String title) {
action.setIcon(getResources().getDrawable(R.drawable.ic_error_red));
action.setTitle(title);
}
private void displayErrorIconDescription(GuidedAction action, String description) {
action.setIcon(getResources().getDrawable(R.drawable.ic_error_red));
action.setDescription(description);
}
}
......@@ -45,7 +45,7 @@
android:text="@string/register_username"
android:textColor="@color/text_color_primary" />
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/ring_username_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
......@@ -53,9 +53,12 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ring_username_txt_box"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
android:layout_marginBottom="16dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/ring_username_availability_image_view"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/ring_username"
......@@ -75,7 +78,26 @@
</com.google.android.material.textfield.TextInputEditText>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/ring_username_availability_image_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:visibility="invisible"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/ring_username_txt_box"
android:layout_marginTop="16dp"/>
<ProgressBar
android:id="@+id/ring_username_availability_spinner"
style="?android:attr/progressBarStyle"
android:layout_width="24dp"
android:layout_height="24dp"
android:visibility="gone"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/ring_username_txt_box"
android:layout_marginTop="16dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<Switch
android:id="@+id/ring_password_switch"
......
......@@ -204,12 +204,14 @@ along with this program; if not, write to the Free Software
<!-- Name registration -->
<string name="error_username_empty">Enter a username</string>
<string name="no_registered_name_for_account">No registered name found for this account</string>
<string name="username_available">Username available !</string>
<string name="register_name">Register name</string>
<string name="trying_to_register_name">Trying to register name</string>
<string name="registered_username">Registered username</string>
<string name="register_username">Register public username (recommended)</string>
<string name="username_already_taken">Username already taken</string>
<string name="invalid_username">Invalid username</string>
<string name="unknown_error">Unknown error. Please check your network connection!</string>
<string name="looking_for_username_availability">Looking for username availability…</string>
<string name="account_status_connecting">Connecting</string>
<string name="account_status_connection_error">Connection error</string>
......
......@@ -34,24 +34,21 @@ import io.reactivex.subjects.PublishSubject;
public class RingAccountCreationPresenter extends RootPresenter<RingAccountCreationView> {
public static final String TAG = RingAccountCreationPresenter.class.getSimpleName();
public static final int PASSWORD_MIN_LENGTH = 6;
private static final int PASSWORD_MIN_LENGTH = 6;
private static final long TYPING_DELAY = 350L;
private final PublishSubject<String> contactQuery = PublishSubject.create();
protected AccountService mAccountService;
@Inject
@Named("UiScheduler")
protected Scheduler mUiScheduler;
private AccountCreationModel mAccountCreationModel;
private boolean isRingUserNameCorrect = false;
private boolean isPasswordCorrect = true;
private boolean isConfirmCorrect = true;
private boolean isRegisterUsernameChecked = true;
private boolean startUsernameAvailabitlityProgressBarAnimation = true;
private String mPasswordConfirm = "";
private final PublishSubject<String> contactQuery = PublishSubject.create();
@Inject
public RingAccountCreationPresenter(AccountService accountService) {
this.mAccountService = accountService;
......@@ -61,28 +58,34 @@ public class RingAccountCreationPresenter extends RootPresenter<RingAccountCreat
public void bindView(RingAccountCreationView view) {
super.bindView(view);
mCompositeDisposable.add(contactQuery
.debounce(350, TimeUnit.MILLISECONDS)
.switchMapSingle(q -> mAccountService.findRegistrationByName("", "", q))
.debounce(TYPING_DELAY, TimeUnit.MILLISECONDS)
.switchMapSingle(q -> mAccountService.
findRegistrationByName("", "", q))
.observeOn(mUiScheduler)
.subscribe(q -> handleBlockchainResult(q.name, q.address, q.state)));
}
public void init(AccountCreationModel accountCreationModel) {
mAccountCreationModel = accountCreationModel;
}
/**
* Called everytime the provided username for the new account changes
* Sends the new value of the username to the ContactQuery subjet and shows the loading
* animation if it has not been started before
* @param userName
*/
public void userNameChanged(String userName) {
if (!userName.isEmpty()) {
mAccountCreationModel.setUsername(userName);
contactQuery.onNext(userName);
isRingUserNameCorrect = false;
getView().enableTextError();
} else {
mAccountCreationModel.setUsername("");
getView().disableTextError();
RingAccountCreationView view = getView();
if (startUsernameAvailabitlityProgressBarAnimation) {
view.updateUsernameAvailability(RingAccountCreationView.
UsernameAvailabilityStatus.LOADING);
startUsernameAvailabitlityProgressBarAnimation = false;
}
checkForms();
}
public void ringCheckChanged(boolean isChecked) {
......@@ -96,6 +99,12 @@ public class RingAccountCreationPresenter extends RootPresenter<RingAccountCreat
public void passwordChanged(String password) {
mAccountCreationModel.setPassword(password);
if (!password.isEmpty() && password.length() < PASSWORD_MIN_LENGTH) {
getView().showInvalidPasswordError(true);
isPasswordCorrect = false;
} else {
getView().showInvalidPasswordError(false);
isPasswordCorrect = true;
if (!password.equals(mPasswordConfirm)) {
getView().showNonMatchingPasswordError(true);
isConfirmCorrect = false;
......@@ -103,12 +112,6 @@ public class RingAccountCreationPresenter extends RootPresenter<RingAccountCreat
getView().showNonMatchingPasswordError(false);
isConfirmCorrect = true;
}
if (!password.isEmpty() && password.length() < PASSWORD_MIN_LENGTH) {
getView().showInvalidPasswordError(true);
isPasswordCorrect = false;
} else {
getView().showInvalidPasswordError(false);
isPasswordCorrect = true;
}
checkForms();
}
......@@ -140,38 +143,50 @@ public class RingAccountCreationPresenter extends RootPresenter<RingAccountCreat
}
private void checkForms() {
getView().enableNextButton(isInputValid());
boolean valid = isInputValid();
getView().enableNextButton(valid);
if(valid && isRingUserNameCorrect)
getView().updateUsernameAvailability(RingAccountCreationView.
UsernameAvailabilityStatus.AVAILABLE);
}
private void handleBlockchainResult(String name, String address, int state) {
RingAccountCreationView view = getView();
//Once we get the result, we can show the loading animation again when the user types
startUsernameAvailabitlityProgressBarAnimation = true;
if (view == null) {
return;
}
if (name == null || name.isEmpty()) {
view.disableTextError();
view.updateUsernameAvailability(RingAccountCreationView.
UsernameAvailabilityStatus.RESET);
isRingUserNameCorrect = false;
} else {
switch (state) {
case 0:
// on found
view.showExistingNameError();
view.updateUsernameAvailability(RingAccountCreationView.
UsernameAvailabilityStatus.ERROR_USERNAME_TAKEN);
isRingUserNameCorrect = false;
break;
case 1:
// invalid name
view.showInvalidNameError();
view.updateUsernameAvailability(RingAccountCreationView.
UsernameAvailabilityStatus.ERROR_USERNAME_INVALID);
isRingUserNameCorrect = false;
break;
case 2:
// available
view.disableTextError();
view.updateUsernameAvailability(RingAccountCreationView.
UsernameAvailabilityStatus.AVAILABLE);
mAccountCreationModel.setUsername(name);
isRingUserNameCorrect = true;
break;
default:
// on error
view.disableTextError();
view.updateUsernameAvailability(RingAccountCreationView.
UsernameAvailabilityStatus.ERROR);
isRingUserNameCorrect = false;
break;
}
......
......@@ -23,13 +23,16 @@ import cx.ring.mvp.AccountCreationModel;
public interface RingAccountCreationView {
void enableTextError();
void disableTextError();
void showExistingNameError();
enum UsernameAvailabilityStatus {
ERROR_USERNAME_TAKEN,
ERROR_USERNAME_INVALID,
ERROR,
LOADING,
AVAILABLE,
RESET
}
void showInvalidNameError();
void updateUsernameAvailability(UsernameAvailabilityStatus status);
void showInvalidPasswordError(boolean display);
......
......@@ -59,12 +59,12 @@ import cx.ring.utils.SwigNativeConverter;
import cx.ring.utils.VCardUtils;
import ezvcard.VCard;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.BehaviorSubject;
import io.reactivex.subjects.PublishSubject;
import io.reactivex.subjects.Subject;
import io.reactivex.Observable;
/**
* This service handles the accounts (Ring and SIP)
......@@ -970,7 +970,7 @@ public class AccountService {
public Single<RegisteredName> findRegistrationByName(final String account, final String nameserver, final String name) {
if (name == null || name.isEmpty()) {
return Single.create(l -> l.onError(new IllegalArgumentException()));
return Single.just(new RegisteredName());
}
return getRegisteredNames()
.filter(r -> account.equals(r.accountId) && name.equals(r.name))
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment