package org.thoughtcrime.securesms; import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.Group; import com.b44t.messenger.DcProvider; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import android.text.TextUtils; import android.util.Patterns; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.ImageView; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEventCenter; import org.thoughtcrime.securesms.connect.ApplicationDcContext; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.IntentUtils; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.SettableFuture; import org.thoughtcrime.securesms.util.views.ProgressDialog; import java.util.concurrent.ExecutionException; import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_ADDRESS; import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_MAIL_PASSWORD; import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_MAIL_PORT; import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_MAIL_SERVER; import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_MAIL_USER; import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_SEND_PASSWORD; import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_SEND_PORT; import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_SEND_SERVER; import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_SEND_USER; import static org.thoughtcrime.securesms.connect.DcHelper.getContext; public class RegistrationActivity extends BaseActionBarActivity implements DcEventCenter.DcEventDelegate { private enum VerificationType { EMAIL, SERVER, PORT, } private final DynamicTheme dynamicTheme = new DynamicTheme(); private TextInputEditText emailInput; private TextInputEditText passwordInput; private View providerLayout; private TextView providerHint; private TextView providerLink; private @Nullable DcProvider provider; private Group advancedGroup; private ImageView advancedIcon; private ProgressDialog progressDialog; private boolean gmailDialogShown; MenuItem loginMenuItem; Spinner imapSecurity; Spinner smtpSecurity; Spinner authMethod; Spinner certCheck; @Override public void onCreate(Bundle bundle) { super.onCreate(bundle); dynamicTheme.onCreate(this); setContentView(R.layout.registration_activity); emailInput = findViewById(R.id.email_text); passwordInput = findViewById(R.id.password_text); providerLayout = findViewById(R.id.provider_layout); providerHint = findViewById(R.id.provider_hint); providerLink = findViewById(R.id.provider_link); providerLink.setOnClickListener(l -> onProviderLink()); advancedGroup = findViewById(R.id.advanced_group); advancedIcon = findViewById(R.id.advanced_icon); TextView advancedTextView = findViewById(R.id.advanced_text); TextInputEditText imapServerInput = findViewById(R.id.imap_server_text); TextInputEditText imapPortInput = findViewById(R.id.imap_port_text); TextInputEditText smtpServerInput = findViewById(R.id.smtp_server_text); TextInputEditText smtpPortInput = findViewById(R.id.smtp_port_text); TextView viewLogText = findViewById(R.id.view_log_button); imapSecurity = findViewById(R.id.imap_security); smtpSecurity = findViewById(R.id.smtp_security); authMethod = findViewById(R.id.auth_method); certCheck = findViewById(R.id.cert_check); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setTitle(R.string.login_header); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); } emailInput.setOnFocusChangeListener((view, focused) -> focusListener(view, focused, VerificationType.EMAIL)); imapServerInput.setOnFocusChangeListener((view, focused) -> focusListener(view, focused, VerificationType.SERVER)); imapPortInput.setOnFocusChangeListener((view, focused) -> focusListener(view, focused, VerificationType.PORT)); smtpServerInput.setOnFocusChangeListener((view, focused) -> focusListener(view, focused, VerificationType.SERVER)); smtpPortInput.setOnFocusChangeListener((view, focused) -> focusListener(view, focused, VerificationType.PORT)); advancedTextView.setOnClickListener(l -> onAdvancedSettings()); advancedIcon.setOnClickListener(l -> onAdvancedSettings()); advancedIcon.setRotation(45); viewLogText.setOnClickListener((view) -> showLog()); boolean isConfigured = DcHelper.isConfigured(getApplicationContext()); if (isConfigured) { TextInputLayout passwordLayout = findViewById(R.id.password); passwordLayout.setPasswordVisibilityToggleEnabled(false); TextInputLayout smtpPasswordLayout = findViewById(R.id.smtp_password); smtpPasswordLayout.setPasswordVisibilityToggleEnabled(false); TextInputEditText imapLoginInput = findViewById(R.id.imap_login_text); String email = DcHelper.get(this, CONFIG_ADDRESS); emailInput.setText(email); if(!TextUtils.isEmpty(email)) { emailInput.setSelection(email.length(), email.length()); } passwordInput.setText(DcHelper.get(this, CONFIG_MAIL_PASSWORD)); imapLoginInput.setText(DcHelper.get(this, CONFIG_MAIL_USER)); imapServerInput.setText(DcHelper.get(this, CONFIG_MAIL_SERVER)); imapPortInput.setText(DcHelper.get(this, CONFIG_MAIL_PORT)); TextInputEditText smtpLoginInput = findViewById(R.id.smtp_login_text); TextInputEditText smtpPasswordInput = findViewById(R.id.smtp_password_text); smtpLoginInput.setText(DcHelper.get(this, CONFIG_SEND_USER)); smtpPasswordInput.setText(DcHelper.get(this, CONFIG_SEND_PASSWORD)); smtpServerInput.setText(DcHelper.get(this, CONFIG_SEND_SERVER)); smtpPortInput.setText(DcHelper.get(this, CONFIG_SEND_PORT)); int serverFlags = DcHelper.getInt(this, "server_flags", 0); int sel = 0; if((serverFlags&DcContext.DC_LP_IMAP_SOCKET_SSL)!=0) sel = 1; if((serverFlags&DcContext.DC_LP_IMAP_SOCKET_STARTTLS)!=0) sel = 2; if((serverFlags&DcContext.DC_LP_IMAP_SOCKET_PLAIN)!=0) sel = 3; imapSecurity.setSelection(sel); sel = 0; if((serverFlags&DcContext.DC_LP_SMTP_SOCKET_SSL)!=0) sel = 1; if((serverFlags&DcContext.DC_LP_SMTP_SOCKET_STARTTLS)!=0) sel = 2; if((serverFlags&DcContext.DC_LP_SMTP_SOCKET_PLAIN)!=0) sel = 3; smtpSecurity.setSelection(sel); sel = 0; if((serverFlags&DcContext.DC_LP_AUTH_OAUTH2)!=0) sel = 1; authMethod.setSelection(sel); int certCheckFlags = DcHelper.getInt(this, "imap_certificate_checks"); certCheck.setSelection(certCheckFlags); } DcHelper.getContext(this).eventCenter.addObserver(DcContext.DC_EVENT_CONFIGURE_PROGRESS, this); } @Override public void onResume() { super.onResume(); dynamicTheme.onResume(this); } private void showLog() { Intent intent = new Intent(getApplicationContext(), LogViewActivity.class); startActivity(intent); } @Override public boolean onPrepareOptionsMenu(Menu menu) { MenuInflater inflater = this.getMenuInflater(); menu.clear(); inflater.inflate(R.menu.registration, menu); loginMenuItem = menu.findItem(R.id.do_register); super.onPrepareOptionsMenu(menu); return true; } public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.do_register) { checkOauth2start().addListener(new ListenableFuture.Listener() { @Override public void onSuccess(Boolean oauth2started) { if(!oauth2started) { onLogin(); } } @Override public void onFailure(ExecutionException e) { onLogin(); } }); return true; } else if (id == android.R.id.home) { // handle close button click here finish(); return true; } return super.onOptionsItemSelected(item); } @Override public void onDestroy() { super.onDestroy(); DcHelper.getContext(this).eventCenter.removeObservers(this); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } private void focusListener(View view, boolean focused, VerificationType type) { if (!focused) { TextInputEditText inputEditText = (TextInputEditText) view; switch (type) { case EMAIL: verifyEmail(inputEditText); checkOauth2start(); break; case SERVER: verifyServer(inputEditText); break; case PORT: verifyPort(inputEditText); break; } } } private long oauth2Requested = 0; private ListenableFuture checkOauth2start() { SettableFuture oauth2started = new SettableFuture<>(); String email = emailInput.getText().toString(); if (!TextUtils.isEmpty(email) ) { // the redirect-uri is also used as intent-filter in the manifest // and should be whitelisted by the supported oauth2 services String redirectUrl = "chat.delta:/"+BuildConfig.APPLICATION_ID+"/auth"; String oauth2url = DcHelper.getContext(this).getOauth2Url(email, redirectUrl); if (!TextUtils.isEmpty(oauth2url)) { new AlertDialog.Builder(this) .setTitle(R.string.login_info_oauth2_title) .setMessage(R.string.login_info_oauth2_text) .setNegativeButton(R.string.cancel, (dialog, which)->{ updateProviderInfo(); oauth2started.set(false); }) .setPositiveButton(R.string.perm_continue, (dialog, which)-> { // pass control to browser, we'll be back in business at (**) oauth2Requested = System.currentTimeMillis(); IntentUtils.showBrowserIntent(this, oauth2url); oauth2started.set(true); }) .setCancelable(false) .show(); } else { updateProviderInfo(); oauth2started.set(false); } } else { updateProviderInfo(); oauth2started.set(false); } return oauth2started; } private void updateProviderInfo() { provider = getContext(this).getProviderFromEmail(emailInput.getText().toString()); if (provider!=null) { Resources res = getResources(); providerHint.setText(provider.getBeforeLoginHint()); switch (provider.getStatus()) { case DcProvider.DC_PROVIDER_STATUS_PREPARATION: providerHint.setTextColor(res.getColor(R.color.provider_prep_fg)); providerLink.setTextColor(res.getColor(R.color.provider_prep_fg)); providerLayout.setBackgroundColor(res.getColor(R.color.provider_prep_bg)); providerLayout.setVisibility(View.VISIBLE); break; case DcProvider.DC_PROVIDER_STATUS_BROKEN: providerHint.setTextColor(res.getColor(R.color.provider_broken_fg)); providerLink.setTextColor(res.getColor(R.color.provider_broken_fg)); providerLayout.setBackgroundColor(getResources().getColor(R.color.provider_broken_bg)); providerLayout.setVisibility(View.VISIBLE); break; default: providerLayout.setVisibility(View.GONE); break; } } else { providerLayout.setVisibility(View.GONE); } } private void onProviderLink() { if (provider!=null) { String url = provider.getOverviewPage(); if(!url.isEmpty()) { try { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); } catch (ActivityNotFoundException e) { Toast.makeText(this, R.string.no_browser_installed, Toast.LENGTH_LONG).show(); } } else { // this should normally not happen Toast.makeText(this, "ErrProviderWithoutUrl", Toast.LENGTH_LONG).show(); } } } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if(Intent.ACTION_VIEW.equals(intent.getAction())) { Uri uri = intent.getData(); String path = uri.getPath(); if(!(path.startsWith("/"+BuildConfig.APPLICATION_ID)||path.startsWith("/auth")) || System.currentTimeMillis()-oauth2Requested > 3*60*60*1000) { return; // timeout after some hours or a request belonging to a bad path. } // back in business after we passed control to the browser in (**) String code = uri.getQueryParameter("code"); if(!TextUtils.isEmpty(code)) { passwordInput.setText(code); authMethod.setSelection(1/*OAuth2*/); onLogin(); } } } private boolean matchesEmailPattern(String email) { return !TextUtils.isEmpty(email) && Patterns.EMAIL_ADDRESS.matcher(email).matches(); } private void verifyEmail(TextInputEditText view) { String error = getString(R.string.login_error_mail); String email = view.getText().toString(); if (!matchesEmailPattern(email)) { view.setError(error); } } private void verifyServer(TextInputEditText view) { String error = getString(R.string.login_error_server); String server = view.getText().toString(); if (!TextUtils.isEmpty(server) && !Patterns.DOMAIN_NAME.matcher(server).matches() && !Patterns.IP_ADDRESS.matcher(server).matches() && !Patterns.WEB_URL.matcher(server).matches()) { view.setError(error); } } private void verifyPort(TextInputEditText view) { String error = getString(R.string.login_error_port); String portString = view.getText().toString(); if (!portString.isEmpty()) { try { int port = Integer.valueOf(portString); if (port < 1 || port > 65535) { view.setError(error); } } catch (NumberFormatException exception) { view.setError(error); } } } private void onAdvancedSettings() { boolean advancedViewVisible = advancedGroup.getVisibility() == View.VISIBLE; if (advancedViewVisible) { advancedGroup.setVisibility(View.GONE); advancedIcon.setRotation(45); } else { advancedGroup.setVisibility(View.VISIBLE); advancedIcon.setRotation(0); } } private void onLogin() { if (!verifyRequiredFields()) { Toast.makeText(this, R.string.login_error_required_fields, Toast.LENGTH_LONG).show(); return; } setupConfig(); if (progressDialog != null) { progressDialog.dismiss(); progressDialog = null; } progressDialog = new ProgressDialog(this); progressDialog.setMessage(getString(R.string.one_moment)); progressDialog.setCanceledOnTouchOutside(false); progressDialog.setCancelable(false); progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getString(android.R.string.cancel), (dialog, which) -> stopLoginProcess()); progressDialog.show(); } private boolean verifyRequiredFields() { String email = emailInput.getText().toString(); return !email.isEmpty() && matchesEmailPattern(email) && !passwordInput.getText().toString().isEmpty(); } private void setupConfig() { setConfig(R.id.email_text, "addr", true); setConfig(R.id.password_text, "mail_pw", false); setConfig(R.id.imap_server_text, "mail_server", true); setConfig(R.id.imap_port_text, "mail_port", true); setConfig(R.id.imap_login_text, "mail_user", false); setConfig(R.id.smtp_server_text, "send_server", true); setConfig(R.id.smtp_port_text, "send_port", true); setConfig(R.id.smtp_login_text, "send_user", false); setConfig(R.id.smtp_password_text, "send_pw", false); int server_flags = 0; if(imapSecurity.getSelectedItemPosition()==1) server_flags |= DcContext.DC_LP_IMAP_SOCKET_SSL; if(imapSecurity.getSelectedItemPosition()==2) server_flags |= DcContext.DC_LP_IMAP_SOCKET_STARTTLS; if(imapSecurity.getSelectedItemPosition()==3) server_flags |= DcContext.DC_LP_IMAP_SOCKET_PLAIN; if(smtpSecurity.getSelectedItemPosition()==1) server_flags |= DcContext.DC_LP_SMTP_SOCKET_SSL; if(smtpSecurity.getSelectedItemPosition()==2) server_flags |= DcContext.DC_LP_SMTP_SOCKET_STARTTLS; if(smtpSecurity.getSelectedItemPosition()==3) server_flags |= DcContext.DC_LP_SMTP_SOCKET_PLAIN; if(authMethod.getSelectedItemPosition()==1) server_flags |= DcContext.DC_LP_AUTH_OAUTH2; DcHelper.getContext(this).setConfigInt("server_flags", server_flags); DcHelper.getContext(this).setConfigInt("smtp_certificate_checks", certCheck.getSelectedItemPosition()); DcHelper.getContext(this).setConfigInt("imap_certificate_checks", certCheck.getSelectedItemPosition()); // calling configure() results in // receiving multiple DC_EVENT_CONFIGURE_PROGRESS events DcHelper.getContext(this).captureNextError(); DcHelper.getContext(this).configure(); } private void setConfig(@IdRes int viewId, String configTarget, boolean doTrim) { TextInputEditText view = findViewById(viewId); String value = view.getText().toString(); if(doTrim) { value = value.trim(); } DcHelper.getContext(this).setConfig(configTarget, value.isEmpty()? null : value); } private void stopLoginProcess() { DcHelper.getContext(this).stopOngoingProcess(); } @Override public void handleEvent(int eventId, Object data1, Object data2) { if (eventId==DcContext.DC_EVENT_CONFIGURE_PROGRESS) { ApplicationDcContext dcContext = DcHelper.getContext(this); long progress = (Long)data1; if (progress==0/*error/aborted*/) { dcContext.endCaptureNextError(); progressDialog.dismiss(); if (dcContext.hasCapturedError()) { new AlertDialog.Builder(this) .setMessage(dcContext.getCapturedError()) .setPositiveButton(android.R.string.ok, null) .show(); } } else if (progress<1000/*progress in permille*/) { int percent = (int)progress / 10; progressDialog.setMessage(getResources().getString(R.string.one_moment)+String.format(" %d%%", percent)); } else if (progress==1000/*done*/) { dcContext.endCaptureNextError(); progressDialog.dismiss(); Intent conversationList = new Intent(getApplicationContext(), ConversationListActivity.class); startActivity(conversationList); finish(); } } } }