package com.zulip.android.activities; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentSender.SendIntentException; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.customtabs.CustomTabsIntent; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; import android.util.Log; import android.util.Patterns; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.Toast; import com.google.android.gms.auth.api.Auth; import com.google.android.gms.auth.api.signin.GoogleSignInAccount; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.gms.auth.api.signin.GoogleSignInResult; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.zulip.android.BuildConfig; import com.zulip.android.R; import com.zulip.android.ZulipApp; import com.zulip.android.networking.AsyncDevGetEmails; import com.zulip.android.networking.ZulipAsyncPushTask; import com.zulip.android.networking.response.LoginResponse; import com.zulip.android.networking.response.ZulipBackendResponse; import com.zulip.android.networking.util.DefaultCallback; import com.zulip.android.util.ActivityTransitionAnim; import com.zulip.android.util.AnimationHelper; import com.zulip.android.util.CommonProgressDialog; import com.zulip.android.util.Constants; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.List; import java.util.Locale; import retrofit2.Call; import retrofit2.Response; /** * Activity to Login through various backends on a specified server. * Currently supported LoginAuths are Emailbackend and DevAuthBackend. */ public class LoginActivity extends BaseActivity implements View.OnClickListener, GoogleApiClient.OnConnectionFailedListener { //region state-restoration static final String USERNAME = "username"; static final String PASSWORD = "password"; static final String SERVER_IN = "serverIn"; private static final String TAG = "LoginActivity"; private static final int REQUEST_CODE_RESOLVE_ERR = 9000; private static final int REQUEST_CODE_SIGN_IN = 9001; private CommonProgressDialog commonProgressDialog; private GoogleApiClient mGoogleApiClient; private EditText mServerEditText; private EditText mUserName; private EditText mPassword; private ImageView mShowPassword; private EditText serverIn; private boolean skipAnimations = false; //endregion private View mGoogleSignInButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.login); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); toolbar.setTitle(R.string.title_login); setSupportActionBar(toolbar); // Progress bar to be displayed if the connection failure is not resolved. commonProgressDialog = new CommonProgressDialog(this); mServerEditText = (EditText) findViewById(R.id.server_url); mGoogleSignInButton = findViewById(R.id.google_sign_in_button); findViewById(R.id.google_sign_in_button).setOnClickListener(this); findViewById(R.id.zulip_login).setOnClickListener(this); mUserName = (EditText) findViewById(R.id.username); mPassword = (EditText) findViewById(R.id.password); mShowPassword = (ImageView) findViewById(R.id.showPassword); serverIn = (EditText) findViewById(R.id.server_url_in); String serverUrl = getIntent().getStringExtra(Constants.SERVER_URL); if (serverUrl != null) { serverIn.setText(serverUrl); } findViewById(R.id.server_btn).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { checkForError(); } }); findViewById(R.id.input_another_server).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { AnimationHelper.hideView(findViewById(R.id.serverInput), 100); AnimationHelper.showView(findViewById(R.id.serverFieldLayout), 201); mServerEditText.setText(""); mServerEditText.setEnabled(false); findViewById(R.id.passwordAuthLayout).setVisibility(View.GONE); findViewById(R.id.google_sign_in_button).setVisibility(View.GONE); findViewById(R.id.local_server_button).setVisibility(View.GONE); //remove error from all editText as user now corrected serverUrl mPassword.setError(null); mUserName.setError(null); serverIn.setError(null); mServerEditText.setError(null); } }); //restore instance state on orientation change if (savedInstanceState != null) { skipAnimations = true; serverIn.setText(savedInstanceState.getString(SERVER_IN)); ((Button) findViewById(R.id.server_btn)).performClick(); mUserName.setText(savedInstanceState.getString(USERNAME)); mPassword.setText(savedInstanceState.getString(PASSWORD)); } mShowPassword.setVisibility(View.GONE); mPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); mPassword.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (mPassword.getText().length() > 0) { mShowPassword.setVisibility(View.VISIBLE); } else { mShowPassword.setVisibility(View.GONE); } } @Override public void afterTextChanged(Editable s) { } }); mShowPassword.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mShowPassword.getTag().toString().equals("visible")) { mShowPassword.setTag("hide"); mShowPassword.setImageResource(R.drawable.ic_visibility_off_black_24dp); mPassword.setInputType(InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); mPassword.setSelection(mPassword.length()); } else { mShowPassword.setTag("visible"); mShowPassword.setImageResource(R.drawable.ic_visibility_black_24dp); mPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); mPassword.setSelection(mPassword.length()); } } }); } private void showLoginFields() { AnimationHelper.showView(findViewById(R.id.serverInput), skipAnimations ? 0 : 201); AnimationHelper.hideView(findViewById(R.id.serverFieldLayout), skipAnimations ? 0 : 100); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); switch (requestCode) { case REQUEST_CODE_SIGN_IN: GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(intent); handleSignInResult(result); break; default: break; } } @Override protected void onStart() { super.onStart(); if (mGoogleApiClient != null) { mGoogleApiClient.connect(); } } @Override protected void onStop() { if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) { mGoogleApiClient.disconnect(); } super.onStop(); } @Override protected void onDestroy() { super.onDestroy(); if (commonProgressDialog != null && commonProgressDialog.isShowing()) { commonProgressDialog.dismiss(); } } private void checkForError() { String serverURL = serverIn.getText().toString(); // trim leading or trailing white spaces in Url serverURL = serverURL.trim(); int errorMessage = R.string.invalid_server_domain; String httpScheme = (BuildConfig.DEBUG) ? "http" : "https"; if (serverURL.isEmpty()) { serverIn.setError(getString(errorMessage)); return; } // add http or https if scheme is not included if (!serverURL.contains("://")) { serverURL = httpScheme + "://" + serverURL; if (BuildConfig.DEBUG) showHTTPDialog(serverURL); //Ask for http or https if in non-prod builds otherwise if in prod then use https else showBackends(httpScheme, serverURL); } else { Uri serverUri = Uri.parse(serverURL); if (!BuildConfig.DEBUG && serverUri.getScheme().equals("http")) { //Production build and not https showHTTPDialog(serverURL); } else { showBackends(serverUri.getScheme(), serverURL); } } } @Override public void onSaveInstanceState(Bundle savedInstanceState) { Boolean inLogin = mUserName.isShown(); savedInstanceState.putString(SERVER_IN, mServerEditText.getText().toString()); savedInstanceState.putString(USERNAME, mUserName.getText().toString()); savedInstanceState.putString(PASSWORD, mPassword.getText().toString()); } private boolean isUrlValid(String url) { if (BuildConfig.DEBUG) { return Patterns.WEB_URL.matcher(String.valueOf(url)).matches() || Patterns.IP_ADDRESS.matcher(url).matches(); } else { return Patterns.WEB_URL.matcher(String.valueOf(url)).matches(); } } private void showBackends(String httpScheme, String serverURL) { commonProgressDialog.showWithMessage(getString(R.string.connecting_to_server)); // if server url does not end with "/", then append it if (!serverURL.endsWith("/")) { serverURL = serverURL + "/"; } if (!isUrlValid(serverURL)) { Toast.makeText(LoginActivity.this, R.string.invalid_url, Toast.LENGTH_SHORT).show(); commonProgressDialog.dismiss(); return; } Uri serverUri = Uri.parse(serverURL); serverUri = serverUri.buildUpon().scheme(httpScheme).build(); // display server url with http scheme used serverIn.setText(serverUri.toString().toLowerCase(Locale.US)); mServerEditText.setText(serverUri.toString().toLowerCase(Locale.US)); mServerEditText.setEnabled(false); // if server url does not end with "api/" or if the path is empty, use "/api" as last segment in the path List<String> paths = serverUri.getPathSegments(); if (paths.isEmpty() || !paths.get(paths.size() - 1).equals("api")) { serverUri = serverUri.buildUpon().appendEncodedPath("api/").build(); } ((ZulipApp) getApplication()).setServerURL(serverUri.toString().toLowerCase(Locale.US)); // create new zulipServices object every time by setting it to null getApp().setZulipServices(null); getServices() .getAuthBackends() .enqueue(new DefaultCallback<ZulipBackendResponse>() { @Override public void onSuccess(Call<ZulipBackendResponse> call, Response<ZulipBackendResponse> response) { View view = LoginActivity.this.getCurrentFocus(); if (view != null) { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(view.getWindowToken(), 0); } if (response.body().isPassword()) { findViewById(R.id.passwordAuthLayout).setVisibility(View.VISIBLE); } if (response.body().isGoogle()) { findViewById(R.id.google_sign_in_button).setVisibility(View.VISIBLE); } if (response.body().isDev()) { findViewById(R.id.local_server_button).setVisibility(View.VISIBLE); } commonProgressDialog.dismiss(); showLoginFields(); } @Override public void onError(Call<ZulipBackendResponse> call, Response<ZulipBackendResponse> response) { Toast.makeText(LoginActivity.this, R.string.toast_login_failed_fetching_backends, Toast.LENGTH_SHORT).show(); commonProgressDialog.dismiss(); } @Override public void onFailure(Call<ZulipBackendResponse> call, Throwable t) { super.onFailure(call, t); if (!isNetworkAvailable()) { Toast.makeText(LoginActivity.this, R.string.toast_no_internet_connection, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(LoginActivity.this, R.string.invalid_url, Toast.LENGTH_SHORT).show(); } commonProgressDialog.dismiss(); } }); } private boolean isNetworkAvailable() { ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); return activeNetworkInfo != null && activeNetworkInfo.isConnected(); } private void showHTTPDialog(final String serverURL) { new AlertDialog.Builder(this) .setTitle(R.string.http_or_https) .setMessage(((BuildConfig.DEBUG) ? R.string.http_message_debug : R.string.http_message)) .setPositiveButton(R.string.use_https, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); showBackends("https", serverURL); } }) .setNeutralButton(R.string.use_http, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); showBackends("http", serverURL); } }) .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { dialog.dismiss(); } }).show(); } private void handleSignInResult(final GoogleSignInResult result) { Log.d("Login", "handleSignInResult:" + result.isSuccess()); if (result.isSuccess()) { GoogleSignInAccount account = result.getSignInAccount(); // if there's a problem with fetching the account, bail if (account == null) { commonProgressDialog.dismiss(); Toast.makeText(LoginActivity.this, R.string.google_app_login_failed, Toast.LENGTH_SHORT).show(); return; } getServices() .login("google-oauth2-token", account.getIdToken()) .enqueue(new DefaultCallback<LoginResponse>() { @Override public void onSuccess(Call<LoginResponse> call, Response<LoginResponse> response) { commonProgressDialog.dismiss(); getApp().setLoggedInApiKey(response.body().getApiKey(), response.body().getEmail()); openHome(); } @Override public void onError(Call<LoginResponse> call, Response<LoginResponse> response) { commonProgressDialog.dismiss(); } @Override public void onFailure(Call<LoginResponse> call, Throwable t) { super.onFailure(call, t); commonProgressDialog.dismiss(); } }); } else { // something bad happened. whoops. commonProgressDialog.dismiss(); Toast.makeText(LoginActivity.this, R.string.google_app_login_failed, Toast.LENGTH_SHORT).show(); } } public void openHome() { // Cancel before leaving activity to avoid leaking windows commonProgressDialog.dismiss(); Intent i = new Intent(this, ZulipActivity.class); startActivity(i); // activity transition animation ActivityTransitionAnim.transition(this); finish(); } @Override public void onConnectionFailed(ConnectionResult result) { if (commonProgressDialog.isShowing()) { // The user clicked the sign-in button already. Start to resolve // connection errors. Wait until onConnected() to dismiss the // connection dialog. if (result.hasResolution()) { try { result.startResolutionForResult(this, REQUEST_CODE_RESOLVE_ERR); } catch (SendIntentException e) { Log.e(TAG, e.getMessage(), e); // Yeah, no idea what to do here. commonProgressDialog.dismiss(); Toast.makeText(LoginActivity.this, R.string.google_app_login_failed, Toast.LENGTH_SHORT).show(); } } else { commonProgressDialog.dismiss(); if (!isNetworkAvailable()) { Toast.makeText(LoginActivity.this, R.string.toast_no_internet_connection, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(LoginActivity.this, R.string.google_app_login_failed, Toast.LENGTH_SHORT).show(); } } } } private void setupGoogleSignIn() { if (mGoogleApiClient == null) { GoogleSignInOptions googleSignInOptions = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestEmail() .requestIdToken(BuildConfig.GOOGLE_CLIENT_ID) .build(); mGoogleApiClient = new GoogleApiClient.Builder(LoginActivity.this) .addApi(Auth.GOOGLE_SIGN_IN_API, googleSignInOptions) .addOnConnectionFailedListener(LoginActivity.this) .build(); mGoogleApiClient.connect(); allowUserToPickAccount(); } else { allowUserToPickAccount(); } } private void allowUserToPickAccount() { Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient); startActivityForResult(signInIntent, REQUEST_CODE_SIGN_IN); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.google_sign_in_button: commonProgressDialog.showWithMessage(getString(R.string.signing_in)); setupGoogleSignIn(); break; case R.id.zulip_login: if (!isInputValid()) { return; } commonProgressDialog.showWithMessage(getString(R.string.signing_in)); String username = mUserName.getText().toString(); String password = mPassword.getText().toString(); getServices() .login(username, password) .enqueue(new DefaultCallback<LoginResponse>() { @Override public void onSuccess(Call<LoginResponse> call, Response<LoginResponse> response) { commonProgressDialog.dismiss(); getApp().setLoggedInApiKey(response.body().getApiKey(), response.body().getEmail()); openHome(); } @Override public void onError(Call<LoginResponse> call, Response<LoginResponse> response) { commonProgressDialog.dismiss(); if (response != null && response.errorBody() != null) { try { JSONObject message = new JSONObject(response.errorBody().string()); Toast.makeText(LoginActivity.this, message.getString("msg"), Toast.LENGTH_LONG).show(); } catch (JSONException | IOException e) { // oops Toast.makeText(LoginActivity.this, R.string.login_activity_toast_login_error, Toast.LENGTH_LONG).show(); } } else { if (!isNetworkAvailable()) { Toast.makeText(LoginActivity.this, R.string.toast_no_internet_connection, Toast.LENGTH_LONG).show(); } else { Toast.makeText(LoginActivity.this, R.string.login_activity_toast_login_error, Toast.LENGTH_LONG).show(); } } } @Override public void onFailure(Call<LoginResponse> call, Throwable t) { super.onFailure(call, t); commonProgressDialog.dismiss(); if (!isNetworkAvailable()) { Toast.makeText(LoginActivity.this, R.string.toast_no_internet_connection, Toast.LENGTH_LONG).show(); } else { Toast.makeText(LoginActivity.this, R.string.login_activity_toast_login_error, Toast.LENGTH_LONG).show(); } } }); break; case R.id.privacy: openUrl(Constants.END_POINT_PRIVACY); break; case R.id.terms: openUrl(Constants.END_POINT_TERMS_OF_SERVICE); break; case R.id.local_server_button: if (!isInputValidForDevAuth()) return; commonProgressDialog.showWithMessage(getString(R.string.signing_in)); AsyncDevGetEmails asyncDevGetEmails = new AsyncDevGetEmails(LoginActivity.this); asyncDevGetEmails.setCallback(new ZulipAsyncPushTask.AsyncTaskCompleteListener() { @Override public void onTaskComplete(String result, JSONObject jsonObject) { commonProgressDialog.dismiss(); } @Override public void onTaskFailure(String result) { commonProgressDialog.dismiss(); } }); asyncDevGetEmails.execute(); break; case R.id.register: openUrl(Constants.END_POINT_REGISTER); break; default: break; } } /** * Open's url in custom tabs if API >= 15 else in browser * @param endPoint of the url */ private void openUrl(String endPoint) { Uri uri; if (serverIn == null || serverIn.getText().toString().isEmpty() || serverIn.getText().toString().equals("")) { return; } else { uri = Uri.parse(serverIn.getText().toString() + endPoint); } if (Build.VERSION.SDK_INT < 15) { Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(intent); return; } CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); CustomTabsIntent intent = builder.build(); intent.launchUrl(LoginActivity.this, uri); } private boolean isInputValidForDevAuth() { boolean isValid = true; if (mServerEditText.length() == 0) { isValid = false; mServerEditText.setError(getString(R.string.server_domain_required)); } else { String serverString = mServerEditText.getText().toString(); if (!serverString.contains("://")) serverString = "https://" + serverString; if (!Patterns.WEB_URL.matcher(serverString).matches()) { mServerEditText.setError(getString(R.string.invalid_domain)); isValid = false; } } return isValid; } private boolean isInputValid() { boolean isValid = true; if (mPassword.length() == 0) { isValid = false; mPassword.setError(getString(R.string.password_required)); } if (mUserName.length() == 0) { isValid = false; mUserName.setError(getString(R.string.username_required)); } if (mServerEditText.length() == 0) { isValid = false; mServerEditText.setError(getString(R.string.server_domain_required)); } else { String serverString = mServerEditText.getText().toString(); if (!serverString.contains("://")) { serverString = "https://" + serverString; } if (!Patterns.WEB_URL.matcher(serverString).matches()) { mServerEditText.setError(getString(R.string.invalid_domain)); isValid = false; } } return isValid; } }