package org.edx.mobile.view; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; import org.edx.mobile.BuildConfig; import org.edx.mobile.R; import org.edx.mobile.authentication.AuthResponse; import org.edx.mobile.authentication.LoginAPI; import org.edx.mobile.base.BaseFragmentActivity; import org.edx.mobile.model.api.FormFieldMessageBody; import org.edx.mobile.model.api.ProfileModel; import org.edx.mobile.model.api.RegisterResponseFieldError; import org.edx.mobile.module.analytics.ISegment; import org.edx.mobile.module.prefs.LoginPrefs; import org.edx.mobile.module.registration.model.RegistrationAgreement; import org.edx.mobile.module.registration.model.RegistrationDescription; import org.edx.mobile.module.registration.model.RegistrationFieldType; import org.edx.mobile.module.registration.model.RegistrationFormField; import org.edx.mobile.module.registration.view.IRegistrationFieldView; import org.edx.mobile.social.SocialFactory; import org.edx.mobile.social.SocialLoginDelegate; import org.edx.mobile.task.RegisterTask; import org.edx.mobile.task.Task; import org.edx.mobile.util.ResourceUtil; import org.edx.mobile.util.images.ErrorUtils; import org.edx.mobile.util.IntentFactory; import org.edx.mobile.view.custom.DividerWithTextView; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; public class RegisterActivity extends BaseFragmentActivity implements SocialLoginDelegate.MobileLoginCallback { private ViewGroup createAccountBtn; private LinearLayout requiredFieldsLayout; private LinearLayout optionalFieldsLayout; private LinearLayout agreementLayout; private TextView createAccountTv; private List<IRegistrationFieldView> mFieldViews = new ArrayList<>(); private SocialLoginDelegate socialLoginDelegate; private View facebookButton; private View googleButton; @Inject LoginPrefs loginPrefs; @NonNull public static Intent newIntent() { return IntentFactory.newIntentForComponent(RegisterActivity.class); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_register); setTitle(R.string.register_title); environment.getSegment().trackScreenView(ISegment.Screens.LAUNCH_ACTIVITY); socialLoginDelegate = new SocialLoginDelegate(this, savedInstanceState, this, environment.getConfig(), loginPrefs); boolean isSocialEnabled = false; facebookButton = findViewById(R.id.facebook_button); googleButton = findViewById(R.id.google_button); if (!SocialFactory.isSocialFeatureEnabled(getApplication(), SocialFactory.SOCIAL_SOURCE_TYPE.TYPE_FACEBOOK, environment.getConfig())) { facebookButton.setVisibility(View.GONE); } else { isSocialEnabled = true; facebookButton.setOnClickListener(socialLoginDelegate.createSocialButtonClickHandler(SocialFactory.SOCIAL_SOURCE_TYPE.TYPE_FACEBOOK)); } if (!SocialFactory.isSocialFeatureEnabled(getApplication(), SocialFactory.SOCIAL_SOURCE_TYPE.TYPE_GOOGLE, environment.getConfig())) { googleButton.setVisibility(View.GONE); } else { isSocialEnabled = true; googleButton.setOnClickListener(socialLoginDelegate.createSocialButtonClickHandler(SocialFactory.SOCIAL_SOURCE_TYPE.TYPE_GOOGLE)); } if (!isSocialEnabled) { findViewById(R.id.panel_social_layout).setVisibility(View.GONE); findViewById(R.id.or_signup_with_email_title).setVisibility(View.GONE); findViewById(R.id.signup_with_row).setVisibility(View.GONE); } TextView agreementMessageView = (TextView) findViewById(R.id.by_creating_account_tv); agreementMessageView.setText(R.string.by_creating_account); createAccountBtn = (ViewGroup) findViewById(R.id.createAccount_button_layout); createAccountBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { createAccount(); } }); createAccountTv = (TextView) findViewById(R.id.create_account_tv); requiredFieldsLayout = (LinearLayout) findViewById(R.id.required_fields_layout); optionalFieldsLayout = (LinearLayout) findViewById(R.id.optional_fields_layout); agreementLayout = (LinearLayout) findViewById(R.id.layout_agreement); final TextView optional_text = (TextView) findViewById(R.id.optional_field_tv); optional_text.setTextColor(optional_text.getLinkTextColors().getDefaultColor()); optional_text.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (optionalFieldsLayout.getVisibility() == View.VISIBLE) { optionalFieldsLayout.setVisibility(v.GONE); optional_text.setText(getString(R.string.show_optional_text)); } else { optionalFieldsLayout.setVisibility(v.VISIBLE); optional_text.setText(getString(R.string.hide_optional_text)); } } }); setupRegistrationForm(); hideSoftKeypad(); tryToSetUIInteraction(true); } public void showAgreement(RegistrationAgreement agreement) { boolean isInAppEULALink = false; try { Uri uri = Uri.parse(agreement.getLink()); if (uri.getScheme().equals("edxapp") && uri.getHost().equals("show_eula")) { isInAppEULALink = true; } } catch (Exception ex) { logger.error(ex); } if (isInAppEULALink) { // show EULA license that is shipped with app environment.getRouter().showWebViewActivity(this, getString(R.string.eula_file_link), getString(R.string.end_user_title)); } else { // for any other link, open agreement link in a webview container environment.getRouter().showWebViewActivity(this, agreement.getLink(), agreement.getText()); } } private void setupRegistrationForm() { try { RegistrationDescription form = environment.getServiceManager().getRegistrationDescription(); LayoutInflater inflater = getLayoutInflater(); List<RegistrationFormField> agreements = new ArrayList<>(); for (RegistrationFormField field : form.getFields()) { if (field.getFieldType().equals(RegistrationFieldType.CHECKBOX) && field.getAgreement() != null) { // this is agreement field // this must be added at the end of the form // hold on it agreements.add(field); } else { IRegistrationFieldView fieldView = IRegistrationFieldView.Factory.getInstance(inflater, field); if (fieldView != null) mFieldViews.add(fieldView); } } // add required and optional fields to the window for (IRegistrationFieldView v : mFieldViews) { if (v.getField().isRequired()) { requiredFieldsLayout.addView(v.getView()); } else { optionalFieldsLayout.addView(v.getView()); } } // add agreement fields to the window if available for (RegistrationFormField agreement : agreements) { IRegistrationFieldView agreementView = IRegistrationFieldView.Factory.getInstance(inflater, agreement); agreementView.setActionListener(new IRegistrationFieldView.IActionListener() { @Override public void onClickAgreement(RegistrationAgreement agreement) { showAgreement(agreement); } }); agreementLayout.addView(agreementView.getView()); } // request rendering of the layouts requiredFieldsLayout.requestLayout(); optionalFieldsLayout.requestLayout(); agreementLayout.requestLayout(); // enable all the views tryToSetUIInteraction(true); } catch (Exception ex) { logger.error(ex); } } private void createAccount() { boolean hasError = false; // prepare query (POST body) Bundle parameters = new Bundle(); for (IRegistrationFieldView v : mFieldViews) { if (v.isValidInput()) { if (v.hasValue()) { // we submit the field only if it provides a value parameters.putString(v.getField().getName(), v.getCurrentValue().getAsString()); } } else { if (!hasError) { // this is the first input field with error, // so focus on it after showing the popup showErrorPopup(v.getView()); } hasError = true; } } // set honor_code and terms_of_service to true parameters.putString("honor_code", "true"); parameters.putString("terms_of_service", "true"); //set parameter required by social registration final String access_token = loginPrefs.getSocialLoginAccessToken(); final String backstore = loginPrefs.getSocialLoginProvider(); boolean fromSocialNet = !TextUtils.isEmpty(access_token); if (fromSocialNet) { parameters.putString("access_token", access_token); parameters.putString("provider", backstore); parameters.putString("client_id", environment.getConfig().getOAuthClientId()); } // do NOT proceed if validations are failed if (hasError) { return; } try { //Send app version in create event String versionName = BuildConfig.VERSION_NAME; String appVersion = String.format("%s v%s", getString(R.string.android), versionName); environment.getSegment().trackCreateAccountClicked(appVersion, backstore); } catch (Exception e) { logger.error(e); } showProgress(); final SocialFactory.SOCIAL_SOURCE_TYPE backsourceType = SocialFactory.SOCIAL_SOURCE_TYPE.fromString(backstore); final RegisterTask task = new RegisterTask(this, parameters, access_token, backsourceType) { @Override public void onSuccess(AuthResponse auth) { onUserLoginSuccess(auth.profile); } @Override public void onException(Exception ex) { hideProgress(); if (ex instanceof LoginAPI.RegistrationException) { final FormFieldMessageBody messageBody = ((LoginAPI.RegistrationException) ex).getFormErrorBody(); boolean errorShown = false; for (String key : messageBody.keySet()) { if (key == null) continue; for (IRegistrationFieldView fieldView : mFieldViews) { if (key.equalsIgnoreCase(fieldView.getField().getName())) { List<RegisterResponseFieldError> error = messageBody.get(key); showErrorOnField(error, fieldView); if (!errorShown) { // this is the first input field with error, // so focus on it after showing the popup showErrorPopup(fieldView.getView()); errorShown = true; } break; } } } if (errorShown) { // We have already shown a specific error message. return; // Return here to avoid falling back to the generic error handler. } } RegisterActivity.this.showAlertDialog(null, ErrorUtils.getErrorMessage(ex, RegisterActivity.this)); } }; task.execute(); } /** * Displays given errors on the given registration field. * * @param errors * @param fieldView * @return */ private void showErrorOnField(List<RegisterResponseFieldError> errors, @NonNull IRegistrationFieldView fieldView) { if (errors != null && !errors.isEmpty()) { StringBuffer buffer = new StringBuffer(); for (RegisterResponseFieldError e : errors) { buffer.append(e.getUserMessage() + " "); } fieldView.handleError(buffer.toString()); } } private void showErrorPopup(@NonNull final View errorView) { showAlertDialog(getResources().getString(R.string.registration_error_title), getResources().getString(R.string.registration_error_message), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { scrollToView((ScrollView) findViewById(R.id.scrollview), errorView); } }); } /** * Scrolls to the top of the given View in the given ScrollView. * * @param scrollView * @param view */ public static void scrollToView(final ScrollView scrollView, final View view) { // View needs a focus view.requestFocus(); // Determine if scroll needs to happen final Rect scrollBounds = new Rect(); scrollView.getHitRect(scrollBounds); if (!view.getLocalVisibleRect(scrollBounds)) { new Handler().post(new Runnable() { @Override public void run() { scrollView.smoothScrollTo(0, view.getTop()); } }); } } // make sure that on the login activity, all errors show up as a dialog as opposed to a flying snackbar @Override public void showAlertDialog(String header, String message) { super.showAlertDialog(header, message); } @Override public boolean createOptionsMenu(Menu menu) { // Register screen doesn't have any menu return true; } /** * we can create enum for strong type, but lose the extensibility. * * @param socialType */ private void showRegularMessage(SocialFactory.SOCIAL_SOURCE_TYPE socialType) { LinearLayout messageLayout = (LinearLayout) findViewById(R.id.message_layout); TextView messageView = (TextView) findViewById(R.id.message_body); //we replace facebook and google programmatically here //in order to make localization work String socialTypeString = ""; String signUpSuccessString = ""; if (socialType == SocialFactory.SOCIAL_SOURCE_TYPE.TYPE_FACEBOOK) { socialTypeString = getString(R.string.facebook_text); signUpSuccessString = getString(R.string.sign_up_with_facebook_ok); } else { //google socialTypeString = getString(R.string.google_text); signUpSuccessString = getString(R.string.sign_up_with_google_ok); } StringBuilder sb = new StringBuilder(); CharSequence extraInfoPrompt = ResourceUtil.getFormattedString(getResources(), R.string.sign_up_with_social_ok, "platform_name", environment.getConfig().getPlatformName()); sb.append(signUpSuccessString.replace(socialTypeString, "<b><strong>" + socialTypeString + "</strong></b>")) .append("<br>").append(extraInfoPrompt); Spanned result = Html.fromHtml(sb.toString()); messageView.setText(result); messageLayout.setVisibility(View.VISIBLE); // UiUtil.animateLayouts(messageLayout); } private void updateUIOnSocialLoginToEdxFailure(SocialFactory.SOCIAL_SOURCE_TYPE socialType, String accessToken) { //change UI. View signupWith = findViewById(R.id.signup_with_row); signupWith.setVisibility(View.GONE); View socialPanel = findViewById(R.id.panel_social_layout); socialPanel.setVisibility(View.GONE); DividerWithTextView signupWithEmailTitle = (DividerWithTextView) findViewById(R.id.or_signup_with_email_title); signupWithEmailTitle.setText(getString(R.string.complete_registration)); //help method showRegularMessage(socialType); //populate the field with value from social site populateEmailFromSocialSite(socialType, accessToken); //hide email and password field for (IRegistrationFieldView field : this.mFieldViews) { String fieldname = field.getField().getName(); if ("password".equalsIgnoreCase(fieldname)) { field.getView().setVisibility(View.GONE); this.mFieldViews.remove(field); break; } } // registrationLayout.requestLayout(); } protected void populateFormField(String fieldName, String value) { for (IRegistrationFieldView field : this.mFieldViews) { if (fieldName.equalsIgnoreCase(field.getField().getName())) { boolean success = field.setRawValue(value); if (success) break; } } } private void populateEmailFromSocialSite(SocialFactory.SOCIAL_SOURCE_TYPE socialType, String accessToken) { this.socialLoginDelegate.getUserInfo(socialType, accessToken, new SocialLoginDelegate.SocialUserInfoCallback() { @Override public void setSocialUserInfo(String email, String name) { populateFormField("email", email); if (name != null && name.length() > 0) { populateFormField("name", name); //Should we save the email here? loginPrefs.setLastAuthenticatedEmail(email); } } }); } ///////section related to social login /////////////// // there are some duplicated code from login activity, as the logic //between login and registration is different subtly @Override protected void onDestroy() { super.onDestroy(); socialLoginDelegate.onActivityDestroyed(); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // outState.putString("username", email_et.getText().toString().trim()); socialLoginDelegate.onActivitySaveInstanceState(outState); } @Override protected void onStop() { super.onStop(); socialLoginDelegate.onActivityStopped(); } @Override protected void onStart() { super.onStart(); // if(email_et.getText().toString().length()==0){ // displayLastEmailId(); // } socialLoginDelegate.onActivityStarted(); } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); socialLoginDelegate.onActivityResult(requestCode, resultCode, data); tryToSetUIInteraction(true); } /** * after login by Facebook or Google, the workflow is different from login page. * we need to adjust the register view * 1. first we try to login, * 2. if login return 200, redirect to course screen. * 3. otherwise, go through the normal registration flow. * * @param accessToken * @param backend */ public void onSocialLoginSuccess(String accessToken, String backend, Task task) { //we should handle UI update here. but right now we do nothing in UI } /* * callback if login to edx success using social access_token */ public void onUserLoginSuccess(ProfileModel profile) { setResult(RESULT_OK); finish(); } /** * callback if login to edx failed using social access_token */ public void onUserLoginFailure(Exception ex, String accessToken, String backend) { // FIXME: We are assuming that if we get here, the accessToken is valid. That may not be the case! //we should redirect to current page. //do nothing //we need to add 1)access_token 2) provider 3) client_id // handle if this is a LoginException tryToSetUIInteraction(true); logger.error(ex); SocialFactory.SOCIAL_SOURCE_TYPE socialType = SocialFactory.SOCIAL_SOURCE_TYPE.fromString(backend); updateUIOnSocialLoginToEdxFailure(socialType, accessToken); } //help functions for UI enable/disable states private void showProgress() { tryToSetUIInteraction(false); View progress = findViewById(R.id.progress_indicator); progress.setVisibility(View.VISIBLE); createAccountTv.setText(getString(R.string.creating_account_text)); } private void hideProgress() { tryToSetUIInteraction(true); View progress = findViewById(R.id.progress_indicator); progress.setVisibility(View.GONE); createAccountTv.setText(getString(R.string.create_account_text)); } //Disable the Create button during server call private void createButtonDisabled() { createAccountBtn.setEnabled(false); createAccountTv.setText(getString(R.string.create_account_text)); } //Enable the Create button during server call private void createButtonEnabled() { createAccountBtn.setEnabled(true); createAccountTv.setText(getString(R.string.create_account_text)); } @Override public boolean tryToSetUIInteraction(boolean enable) { if (enable) { unblockTouch(); createButtonEnabled(); } else { blockTouch(); createButtonDisabled(); } for (IRegistrationFieldView v : mFieldViews) { v.setEnabled(enable); } facebookButton.setClickable(enable); googleButton.setClickable(enable); return true; } }