Skip to content

Instantly share code, notes, and snippets.

@Wajahat-Jawaid
Created September 4, 2019 06:19
Show Gist options
  • Save Wajahat-Jawaid/a9e314697787be5fa41463232bce0434 to your computer and use it in GitHub Desktop.
Save Wajahat-Jawaid/a9e314697787be5fa41463232bce0434 to your computer and use it in GitHub Desktop.
Based on the MVP structure, this is the presenter which handles login and registration through Email
package com.moblibo.pictabite.presenters.login;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.databinding.ObservableField;
import android.os.Bundle;
import android.support.annotation.StringRes;
import android.view.View;
import com.annimon.stream.Stream;
import com.bumptech.glide.load.HttpException;
import com.google.gson.Gson;
import com.my_package.confidential.BuildConfig;
import com.my_package.confidential.R;
import com.my_package.confidential.app.AppConstants;
import com.my_package.confidential.databinding.ViewLoginEmailPanelBinding;
import com.my_package.confidential.exceptions.PBErrorException;
import com.my_package.confidential.models.JSONKeys;
import com.my_package.confidential.models.responses.PBUserResponse;
import com.my_package.confidential.services.PBAddressService;
import com.my_package.confidential.services.PBUserService;
import com.my_package.confidential.services.http.PBHTTPErrors;
import com.my_package.confidential.services.http.PBSessionManager;
import com.my_package.confidential.services.http.SharedPrefsCookiePersistor;
import com.my_package.confidential.utils.DialogUtils;
import com.my_package.confidential.utils.Preferences;
import com.my_package.confidential.utils.UIComponentStyleHelper;
import com.my_package.confidential.utils.UserSession;
import com.my_package.confidential.utils.animation.ViewAnimationUtils;
import com.my_package.confidential.utils.dataBinding.BindableCharSequence;
import com.my_package.confidential.utils.keyboardEvent.KeyboardHeightObserver;
import com.my_package.confidential.utils.keyboardEvent.KeyboardHeightProvider;
import com.my_package.confidential.utils.validators.EmailValidator;
import com.my_package.confidential.utils.validators.GreaterThanValidator;
import com.my_package.confidential.utils.validators.MatchValidator;
import com.my_package.confidential.BuildConfig;
import com.my_package.confidential.R;
import com.my_package.confidential.app.AppConstants;
import com.my_package.confidential.databinding.ViewLoginEmailPanelBinding;
import com.my_package.confidential.exceptions.PBErrorException;
import com.my_package.confidential.models.JSONKeys;
import com.my_package.confidential.models.responses.PBUserResponse;
import com.my_package.confidential.services.PBAddressService;
import com.my_package.confidential.services.PBUserService;
import com.my_package.confidential.services.http.PBHTTPErrors;
import com.my_package.confidential.services.http.PBSessionManager;
import com.my_package.confidential.utils.DialogUtils;
import com.my_package.confidential.utils.UIComponentStyleHelper;
import com.my_package.confidential.utils.UserSession;
import com.my_package.confidential.utils.dataBinding.BindableCharSequence;
import com.my_package.confidential.utils.keyboardEvent.KeyboardHeightObserver;
import com.my_package.confidential.utils.keyboardEvent.KeyboardHeightProvider;
import com.my_package.confidential.utils.validators.EmailValidator;
import com.my_package.confidential.utils.validators.GreaterThanValidator;
import com.my_package.confidential.utils.validators.MatchValidator;
import com.my_package.confidential.utils.validators.RangeValidator;
import java.util.List;
import javax.inject.Inject;
import okhttp3.Cookie;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import rx.subjects.PublishSubject;
import timber.log.Timber;
/**
* Created by Wajahat Jawaid(wajahat.jawaid@confidential.com)
*/
public class LoginRegisterEmailPresenter extends LoginTypesParentPresenter implements KeyboardHeightObserver {
/**
* In milliseconds
*/
private static final int FIELDS_SCALE_DURATION = 100;
/**
* Boolean to track if registration is done from the current view or from
* {@link LoginAccountConfirmationPresenter}
*/
private boolean isRegisterFromCurrentView = true;
/**
* We need to translateY in negative when the keyboard is shown. So translation
* has to be done for the same height as of keyboard but we also have to minify the
* margin taken by changeStateLayout at the bottom, so on keyboard change event, we
* calculate the final value for translationY by subtracting the bottom margin value
* from the keyboard height
*/
private static int keyboardHeightMinusChangeStateLayoutBottomMargin;
/**
* Difference between the margin of changeStateLayout with keyboard opened and closed
*/
private static int changeStateLayoutReducedBottomMargin;
/**
* As form height needs to be scaled, we are calculating the maximum possible height
* of the form when keyboard is opened
*/
private static int screenHeight, parentLayoutHeight, formActualHeight, formMaxHeight, fieldsActualHeight;
/**
* When the compression on height is done due to the keyboard opening
*/
private static int fieldsUpdatedHeight;
private boolean mIsAnimating = false;
public BindableCharSequence firstName = new BindableCharSequence();
public BindableCharSequence email = new BindableCharSequence();
public BindableCharSequence password = new BindableCharSequence();
public BindableCharSequence passwordRepeat = new BindableCharSequence();
private LoginRegisterPresenter parentPresenter;
private PBUserService userService;
private Preferences preferences;
private DialogUtils dialogUtils;
private ViewAnimationUtils animationUtils;
private KeyboardHeightProvider keyboardHeightProvider;
@Inject
protected PBSessionManager sessionManager;
@Inject
protected PBAddressService addressService;
@Inject
protected UserSession userSession;
@Inject
protected LoginFacebookPresenter facebookPresenter;
@Inject
protected LoginRegisterEmailPresenter emailPresenter;
@Inject
protected LoginAccountConfirmationPresenter accountConfirmationPresenter;
@Inject
protected UIComponentStyleHelper styleHelper;
public enum ViewState {
LOGIN,
REGISTER
}
private PublishSubject<Observable<PBUserResponse>> serverCallSubject = PublishSubject.create();
PublishSubject<Observable<PBUserResponse>> getServerCallSubject() {
return serverCallSubject;
}
/**
* Observer to notify the parent presenter of view state changed. It happens
* when registration is successfully done and email activation view has to be shown
*/
private PublishSubject<CharSequence> showEmailConfirmationViewSubject = PublishSubject.create();
PublishSubject<CharSequence> getShowEmailConfirmationViewSubject() {
return showEmailConfirmationViewSubject;
}
/**
* Observer to notify the parent presenter of view state changed. It happens
* when registration is successfully done and email activation view has to be shown
* True for opened, false for closed
*/
private PublishSubject<Integer> keyboardStateSubject = PublishSubject.create();
PublishSubject<Integer> getKeyboardStateChangedSubject() {
return keyboardStateSubject;
}
/**
* Observer to notify the confirmation presenter {@link LoginAccountConfirmationPresenter}
* when registration is successfully done.
*/
private PublishSubject<CharSequence> emailChangeSubject = PublishSubject.create();
PublishSubject<CharSequence> getEmailChangeSubject() {
return emailChangeSubject;
}
private ViewLoginEmailPanelBinding binding;
/**
* Whether the form is of Login or Register at the moment in focus
*/
public ObservableField<ViewState> currentFormState = new ObservableField<>(ViewState.LOGIN);
public LoginRegisterEmailPresenter(LoginRegisterPresenter presenter, PBUserService userService,
DialogUtils dialogUtils, Preferences preferences,
ViewAnimationUtils animationUtils) {
this.userService = userService;
this.parentPresenter = presenter;
this.dialogUtils = dialogUtils;
this.preferences = preferences;
this.animationUtils = animationUtils;
}
public void setBinding(ViewLoginEmailPanelBinding binding) {
this.binding = binding;
}
public ViewLoginEmailPanelBinding getBinding() {
return binding;
}
public void onResume() {
keyboardHeightProvider.setKeyboardHeightObserver(this);
}
public void onPause() {
keyboardHeightProvider.setKeyboardHeightObserver(null);
}
public void onDestroy() {
keyboardHeightProvider.close();
}
public void goToLogin(View v) {
switchViewState(ViewState.LOGIN);
}
public void goToRegister(View v) {
switchViewState(ViewState.REGISTER);
}
/**
* Login action
*
* @param view the button passed from view
*/
public void login(View view) {
if (!validate()) {
return;
}
String email = this.email.get().toString();
String password = this.password.get().toString();
Observable<PBUserResponse> loginObservable = userService.loginWithEmail(email, password, false)
.observeOn(Schedulers.io())
.compose(parentPresenter.getFragment().bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread());
parentPresenter.callServer(this, loginObservable);
}
/**
* Register action from view. Registration can be done from any of the two ways
* 1 - From current view sign up button
* 2 - From {@link LoginAccountConfirmationPresenter} action to update email address
*
* @param view the button passed from view
*/
public void registerFromView(View view) {
if (!validate()) {
return;
}
String firstName = this.firstName.get().toString();
String email = this.email.get().toString();
String password = this.password.get().toString();
callRegisterApi(firstName, email, password);
}
@Override
public void onKeyboardHeightChanged(int height, int orientation) {
if (height > 0) {
keyboardHeightMinusChangeStateLayoutBottomMargin = height - changeStateLayoutReducedBottomMargin;
// View has to be slide up, so it's in negative
keyboardStateSubject.onNext(-1 * keyboardHeightMinusChangeStateLayoutBottomMargin);
int diff = calculateVisibleAreaHeightWhenKeyboardShowing();
if (diff < 0) {
scaleDownFormFieldsHeight();
}
} else {
keyboardStateSubject.onNext(0);
animationUtils.expandToHeight(fieldsUpdatedHeight, fieldsActualHeight, FIELDS_SCALE_DURATION,
binding.nameContainer, binding.emailContainer, binding.passwordContainer, binding.confirmPasswordContainer);
binding.formContainer.requestLayout();
}
}
/* PUBLIC METHODS ENDS */
/* PACKAGE PRIVATE METHODS STARTS */
void initKeyboardProvider() {
keyboardHeightProvider = new KeyboardHeightProvider(parentPresenter.getActivity());
binding.parentContainer.post(() -> keyboardHeightProvider.start());
}
void showWithViewState(ViewState state) {
currentFormState.set(state);
if (state == ViewState.REGISTER) {
binding.nameContainer.setVisibility(View.VISIBLE);
binding.confirmPasswordContainer.setVisibility(View.VISIBLE);
}
}
void registerWhenEmailChangeRequired(String emailText) {
isRegisterFromCurrentView = false;
email.set(emailText);
if (new EmailValidator(email).isValid())
callRegisterApi(preferences.getUsername(), emailText, preferences.getPassword());
else {
dialogUtils.showAlertDialog(R.string.invalidEmail_body);
}
}
/**
* Sets the screen height. Can't calculate the screen height in this presenter
* as the current view is included in the parent presenter
*/
void setScreenHeight(int height) {
screenHeight = height;
}
void calculateViewDimensions() {
binding.parentContainer.post(() -> parentLayoutHeight = binding.parentContainer.getHeight());
binding.formContainer.post(() -> formActualHeight = binding.formContainer.getHeight());
final Context context = binding.getRoot().getContext();
float changeStateMarginWithKeyboardClosed = context.getResources().getDimension(R.dimen.padding_big);
float changeStateMarginWithKeyboardOpened = context.getResources().getDimension(R.dimen.padding_nano);
changeStateLayoutReducedBottomMargin =
(int) (changeStateMarginWithKeyboardClosed - changeStateMarginWithKeyboardOpened);
getLoginFieldsHeight();
}
/* PACKAGE PRIVATE METHODS ENDS */
/* PRIVATE METHODS STARTS */
private void getLoginFieldsHeight() {
fieldsActualHeight = (int) binding.getRoot().getContext().getResources().getDimension(R.dimen.login_fieldContainer_height);
// Initialize with the actual height instead of 0
fieldsUpdatedHeight = fieldsActualHeight;
}
private void switchViewState(ViewState state) {
final int FIELDS_SHOW_HIDE_DURATION = 250;
currentFormState.set(state);
if (state == ViewState.LOGIN) {
animationUtils.collapse(binding.nameContainer, FIELDS_SHOW_HIDE_DURATION);
animationUtils.collapse(binding.confirmPasswordContainer, FIELDS_SHOW_HIDE_DURATION);
} else {
animationUtils.expand(binding.nameContainer, fieldsActualHeight, FIELDS_SHOW_HIDE_DURATION);
animationUtils.expand(binding.confirmPasswordContainer, fieldsActualHeight, FIELDS_SHOW_HIDE_DURATION);
}
}
/**
* Called either from the current binding view or from
* {@link LoginAccountConfirmationPresenter} action to update email address
*/
private void callRegisterApi(String name, String email, String password) {
Observable<PBUserResponse> registerObservable = userService.register(name, email, password)
.observeOn(Schedulers.io())
.compose(parentPresenter.getFragment().bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread());
parentPresenter.callServer(this, registerObservable);
}
private boolean validate() {
email.set(binding.emailText.getText().toString());
password.set(binding.passwordText.getText().toString());
firstName.set(binding.firstNameText.getText().toString());
passwordRepeat.set(binding.passwordRepeatText.getText().toString());
boolean isEmailValid = new EmailValidator(email).isValid();
if (!isEmailValid) {
dialogUtils.showAlertDialog(R.string.invalidEmail_body);
return false;
}
boolean isPasswordValid = new RangeValidator(password, 6, 20).isValid();
if (!isPasswordValid) {
dialogUtils.showAlertDialog(R.string.login_password_error);
return false;
}
if (currentFormState.get() == ViewState.REGISTER) {
boolean isFirstNameValid = new GreaterThanValidator(firstName, 1).isValid();
if (!isFirstNameValid) {
dialogUtils.showAlertDialog(R.string.login_firstName_error);
return false;
}
boolean doPasswordMatch = new MatchValidator(password, passwordRepeat).isValid();
if (!doPasswordMatch) {
dialogUtils.showAlertDialog(R.string.login_passwordRepeat_error);
return false;
}
}
return true;
}
private void callServer(Observable<PBUserResponse> loginOrRegister) {
parentPresenter.getFragment().showLoadingIndicator();
loginOrRegister
.observeOn(Schedulers.io())
.compose(parentPresenter.getFragment().bindToLifecycle())
.flatMap(userResponse -> {
Timber.d("Login Response: %s", new Gson().toJson(userResponse));
parentPresenter.userSession.setUser(userResponse);
if (parentPresenter.userSession.getLocalAddress() != null) {
return parentPresenter.addressService.createAddress(parentPresenter.userSession.getLocalAddress());
}
return Observable.empty();
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(address -> {
parentPresenter.userSession.setLocalAddress(null);
}, throwable -> {
Timber.e("throwable: %s", throwable.getMessage());
parentPresenter.getFragment().hideLoadingIndicator();
// OLD CONDITION
// if (!(throwable instanceof HttpException)) {
// return;
// }
if (throwable instanceof HttpException) {
return;
}
@StringRes int titleRes = R.string.genericError_title;
@StringRes int bodyRes = R.string.remoteDataAccessError_body;
if (throwable instanceof PBErrorException) {
PBErrorException pbErrorException = (PBErrorException) throwable;
Timber.e("PB Error code: %s", pbErrorException.getErrors().get(0).getCode());
switch (pbErrorException.getErrors().get(0).getCode()) {
case PBHTTPErrors.AUTHENTICATION_FAILED:
case PBHTTPErrors.UNKNOWN_USER:
bodyRes = R.string.invalidCredentialsError_body;
break;
case PBHTTPErrors.TOO_MANY_AUTH_ATTEMPTS:
bodyRes = R.string.tooManyAuthAttemptsError_body;
break;
case PBHTTPErrors.ENTRY_ALREADY_EXISTS:
bodyRes = R.string.usernameExistsError_body;
break;
case PBHTTPErrors.VALIDATION_ERROR:
bodyRes = R.string.validationError_body;
break;
case PBHTTPErrors.EMAIL_HOST_UNREACHABLE:
bodyRes = R.string.emailHostUnreachableError_body;
break;
}
}
parentPresenter.dialogUtils.showAlertDialog(bodyRes);
}, () -> {
});
}
@Override
public void onServerCallCompleted(Bundle bundle) {
Timber.d("Server call completed in Email Presenter");
preferences.setUsername(firstName.get().toString());
preferences.setPassword(password.get().toString());
parentPresenter.getFragment().hideLoadingIndicator();
if (currentFormState.get() == ViewState.REGISTER) {
if (isRegisterFromCurrentView) {
// Show email confirmation view
showEmailConfirmationViewSubject.onNext(email.get());
} else {
emailChangeSubject.onNext(email.get());
dialogUtils.showAlertDialog(R.string.account_confirmation_changeEmail_dialog_text);
}
} else {
if (bundle.containsKey(AppConstants.HAS_COMPLETED_REGISTRATION)) {
if (!bundle.getBoolean(AppConstants.HAS_COMPLETED_REGISTRATION)) {
// Hide email confirmation view
slideForward(binding.getRoot());
} else {
// parentPresenter.getFragment().dismiss();
}
}
}
}
private static int calculateVisibleAreaHeightWhenKeyboardShowing() {
/*
Excluding form, the total combined height of other views including
layoutParams inside the parent. This height is calculated to perform scaling
on formActualHeight as we won't be scaling the other views. So getting to know it gives
the hint about how much do we need to scale down formActualHeight
*/
int parentOtherViewsCombinedHeight = parentLayoutHeight - formActualHeight;
formMaxHeight = screenHeight - keyboardHeightMinusChangeStateLayoutBottomMargin - parentOtherViewsCombinedHeight;
return screenHeight - (keyboardHeightMinusChangeStateLayoutBottomMargin + parentLayoutHeight);
}
private void scaleDownFormFieldsHeight() {
// initialize with 0.95f because at this moment we already know that compression
// has to be done initializing with default value i.e. 1.0f will waste that iteration
for (float fieldsScale = 0.95f; fieldsScale > 0.4f; fieldsScale -= 0.05f) {
int formNewHeight = (int) (formActualHeight * fieldsScale);
if (formNewHeight < formMaxHeight) {
fieldsUpdatedHeight = (int) (fieldsActualHeight * fieldsScale);
animationUtils.collapseToHeight(fieldsUpdatedHeight, FIELDS_SCALE_DURATION, binding.nameContainer,
binding.emailContainer, binding.passwordContainer, binding.confirmPasswordContainer);
binding.formContainer.requestLayout();
break;
}
}
}
private List<Cookie> getCookies(Context context) {
SharedPrefsCookiePersistor prefs = new SharedPrefsCookiePersistor(context);
return prefs.loadAll();
}
private String getAppToken(Context context) {
List<Cookie> cookies = getCookies(context);
return Stream.of(cookies)
.filter(c -> c.name().equalsIgnoreCase(JSONKeys.pbToken) &&
c.domain().equalsIgnoreCase(BuildConfig.PB_HOST))
.map(Cookie::value)
.single();
}
/**
* This method animates the email presenter into the foreground by both
* scaling and rotating the binding's view. Upon the completion of
* this animation, the image confirmation presenter regains focus since this method is
* called from the onBackStackChanged method.
*/
private void slideForward(View view) {
final Context context = binding.getRoot().getContext();
PropertyValuesHolder rotateX = PropertyValuesHolder.ofFloat("rotationX", 40f);
PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f);
PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f);
ObjectAnimator movingFragmentAnimator = ObjectAnimator.
ofPropertyValuesHolder(view, rotateX, scaleX, scaleY);
ObjectAnimator movingFragmentRotator = ObjectAnimator.
ofFloat(view, "rotationX", 0);
movingFragmentRotator.setStartDelay(
context.getResources().getInteger(R.integer.half_slide_up_down_duration));
AnimatorSet s = new AnimatorSet();
s.playTogether(movingFragmentAnimator, movingFragmentRotator);
s.setStartDelay(context.getResources().getInteger(R.integer.slide_up_down_duration));
s.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mIsAnimating = false;
}
});
s.start();
}
/* PRIVATE METHODS ENDS */
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment