mastodon-app-ufficiale-android/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/SignupFragment.java

413 lines
14 KiB
Java

package org.joinmastodon.android.fragments.onboarding;
import android.app.ProgressDialog;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.Html;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonDetailedErrorResponse;
import org.joinmastodon.android.api.requests.accounts.RegisterAccount;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.api.requests.oauth.GetOauthToken;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.ui.text.LinkSpan;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.NodeVisitor;
import org.parceler.Parcels;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import androidx.annotation.Nullable;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class SignupFragment extends ToolbarFragment{
private static final String TAG="SignupFragment";
private Instance instance;
private EditText displayName, username, email, password, passwordConfirm, reason;
private FloatingHintEditTextLayout displayNameWrap, usernameWrap, emailWrap, passwordWrap, passwordConfirmWrap, reasonWrap;
private TextView reasonExplain;
private Button btn;
private View buttonBar;
private TextWatcher buttonStateUpdater=new SimpleTextWatcher(e->updateButtonState());
private APIRequest currentBackgroundRequest;
private Application apiApplication;
private Token apiToken;
private boolean submitAfterGettingToken;
private ProgressDialog progressDialog;
private HashSet<EditText> errorFields=new HashSet<>();
private ElevationOnScrollListener onScrollListener;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
createAppAndGetToken();
setTitle(R.string.signup_title);
}
@Nullable
@Override
public View onCreateContentView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_signup, container, false);
TextView domain=view.findViewById(R.id.domain);
displayName=view.findViewById(R.id.display_name);
username=view.findViewById(R.id.username);
email=view.findViewById(R.id.email);
password=view.findViewById(R.id.password);
passwordConfirm=view.findViewById(R.id.password_confirm);
reason=view.findViewById(R.id.reason);
reasonExplain=view.findViewById(R.id.reason_explain);
displayNameWrap=view.findViewById(R.id.display_name_wrap);
usernameWrap=view.findViewById(R.id.username_wrap);
emailWrap=view.findViewById(R.id.email_wrap);
passwordWrap=view.findViewById(R.id.password_wrap);
passwordConfirmWrap=view.findViewById(R.id.password_confirm_wrap);
reasonWrap=view.findViewById(R.id.reason_wrap);
domain.setText('@'+instance.uri);
username.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
username.getViewTreeObserver().removeOnPreDrawListener(this);
username.setPadding(username.getPaddingLeft(), username.getPaddingTop(), domain.getWidth(), username.getPaddingBottom());
return true;
}
});
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
updateButtonState();
username.addTextChangedListener(buttonStateUpdater);
email.addTextChangedListener(buttonStateUpdater);
password.addTextChangedListener(buttonStateUpdater);
passwordConfirm.addTextChangedListener(buttonStateUpdater);
reason.addTextChangedListener(buttonStateUpdater);
username.addTextChangedListener(new ErrorClearingListener(username));
email.addTextChangedListener(new ErrorClearingListener(email));
password.addTextChangedListener(new ErrorClearingListener(password));
passwordConfirm.addTextChangedListener(new ErrorClearingListener(passwordConfirm));
reason.addTextChangedListener(new ErrorClearingListener(reason));
if(!instance.approvalRequired){
reason.setVisibility(View.GONE);
reasonExplain.setVisibility(View.GONE);
}
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.findViewById(R.id.scroller).setOnScrollChangeListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
}
private void onButtonClick(){
if(!password.getText().toString().equals(passwordConfirm.getText().toString())){
passwordConfirmWrap.setErrorState(getString(R.string.signup_passwords_dont_match));
return;
}
showProgressDialog();
if(currentBackgroundRequest!=null){
submitAfterGettingToken=true;
}else if(apiApplication==null){
submitAfterGettingToken=true;
createAppAndGetToken();
}else if(apiToken==null){
submitAfterGettingToken=true;
getToken();
}else{
submit();
}
}
private void submit(){
actuallySubmit();
}
private void actuallySubmit(){
String username=this.username.getText().toString().trim();
String email=this.email.getText().toString().trim();
for(EditText edit:errorFields){
edit.setError(null);
}
errorFields.clear();
new RegisterAccount(username, email, password.getText().toString(), getResources().getConfiguration().locale.getLanguage(), reason.getText().toString())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Token result){
progressDialog.dismiss();
Account fakeAccount=new Account();
fakeAccount.acct=fakeAccount.username=username;
fakeAccount.id="tmp"+System.currentTimeMillis();
fakeAccount.displayName=displayName.getText().toString();
AccountSessionManager.getInstance().addAccount(instance, result, fakeAccount, apiApplication, new AccountActivationInfo(email, System.currentTimeMillis()));
Bundle args=new Bundle();
args.putString("account", AccountSessionManager.getInstance().getLastActiveAccountID());
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
}
@Override
public void onError(ErrorResponse error){
if(error instanceof MastodonDetailedErrorResponse derr){
Map<String, List<MastodonDetailedErrorResponse.FieldError>> fieldErrors=derr.detailedErrors;
boolean first=true;
boolean anyFieldsSkipped=false;
for(String fieldName:fieldErrors.keySet()){
EditText field=getFieldByName(fieldName);
if(field==null){
anyFieldsSkipped=true;
continue;
}
List<MastodonDetailedErrorResponse.FieldError> errors=Objects.requireNonNull(fieldErrors.get(fieldName));
if(errors.size()==1){
getFieldWrapByName(fieldName).setErrorState(getErrorDescription(errors.get(0), fieldName));
}else{
SpannableStringBuilder ssb=new SpannableStringBuilder();
boolean firstErr=true;
for(MastodonDetailedErrorResponse.FieldError err:errors){
if(firstErr){
firstErr=false;
}else{
ssb.append('\n');
}
ssb.append(getErrorDescription(err, fieldName));
}
getFieldWrapByName(fieldName).setErrorState(getErrorDescription(errors.get(0), fieldName));
}
errorFields.add(field);
if(first){
first=false;
field.requestFocus();
}
}
if(anyFieldsSkipped)
error.showToast(getActivity());
}else{
error.showToast(getActivity());
}
progressDialog.dismiss();
}
})
.exec(instance.uri, apiToken);
}
private CharSequence getErrorDescription(MastodonDetailedErrorResponse.FieldError error, String fieldName){
return switch(fieldName){
case "email" -> switch(error.error){
case "ERR_BLOCKED" -> {
String emailAddr=email.getText().toString();
String s=getResources().getString(R.string.signup_email_domain_blocked, TextUtils.htmlEncode(instance.uri), TextUtils.htmlEncode(emailAddr.substring(emailAddr.lastIndexOf('@')+1)));
SpannableStringBuilder ssb=new SpannableStringBuilder();
Jsoup.parseBodyFragment(s).body().traverse(new NodeVisitor(){
private int spanStart;
@Override
public void head(Node node, int depth){
if(node instanceof TextNode tn){
ssb.append(tn.text());
}else if(node instanceof Element){
spanStart=ssb.length();
}
}
@Override
public void tail(Node node, int depth){
if(node instanceof Element){
ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
});
yield ssb;
}
default -> error.description;
};
default -> error.description;
};
}
private EditText getFieldByName(String name){
return switch(name){
case "email" -> email;
case "username" -> username;
case "password" -> password;
case "reason" -> reason;
default -> null;
};
}
private FloatingHintEditTextLayout getFieldWrapByName(String name){
return switch(name){
case "email" -> emailWrap;
case "username" -> usernameWrap;
case "password" -> passwordWrap;
case "reason" -> reasonWrap;
default -> null;
};
}
private void showProgressDialog(){
if(progressDialog==null){
progressDialog=new ProgressDialog(getActivity());
progressDialog.setMessage(getString(R.string.loading));
progressDialog.setCancelable(false);
}
progressDialog.show();
}
private void updateButtonState(){
btn.setEnabled(username.length()>0 && email.length()>0 && email.getText().toString().contains("@") && password.length()>=8 && passwordConfirm.length()>=8 && (!instance.approvalRequired || reason.length()>0));
}
private void createAppAndGetToken(){
currentBackgroundRequest=new CreateOAuthApp()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Application result){
apiApplication=result;
getToken();
}
@Override
public void onError(ErrorResponse error){
currentBackgroundRequest=null;
if(submitAfterGettingToken){
submitAfterGettingToken=false;
progressDialog.dismiss();
error.showToast(getActivity());
}
}
})
.execNoAuth(instance.uri);
}
private void getToken(){
currentBackgroundRequest=new GetOauthToken(apiApplication.clientId, apiApplication.clientSecret, null, GetOauthToken.GrantType.CLIENT_CREDENTIALS)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Token result){
currentBackgroundRequest=null;
apiToken=result;
if(submitAfterGettingToken){
submitAfterGettingToken=false;
submit();
}
}
@Override
public void onError(ErrorResponse error){
currentBackgroundRequest=null;
if(submitAfterGettingToken){
submitAfterGettingToken=false;
progressDialog.dismiss();
error.showToast(getActivity());
}
}
})
.execNoAuth(instance.uri);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
private void onGoBackLinkClick(LinkSpan span){
setResult(false, null);
Nav.finish(this);
}
private class ErrorClearingListener implements TextWatcher{
public final EditText editText;
private ErrorClearingListener(EditText editText){
this.editText=editText;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
}
@Override
public void afterTextChanged(Editable s){
if(errorFields.contains(editText)){
errorFields.remove(editText);
editText.setError(null);
}
}
}
}