/* * Android ownCloud News * * @author David Luhmer * @copyright 2013 David Luhmer david-dev@live.de * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE * License as published by the Free Software Foundation; either * version 3 of the License, or any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU AFFERO GENERAL PUBLIC LICENSE for more details. * * You should have received a copy of the GNU Affero General Public * License along with this library. If not, see . * */ package de.luhmer.owncloudnewsreader; import static java.util.Objects.requireNonNull; import static de.luhmer.owncloudnewsreader.Constants.MIN_NEXTCLOUD_FILES_APP_VERSION_CODE; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ProgressDialog; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.Bundle; import android.text.Editable; import android.text.InputType; import android.text.SpannableString; import android.text.TextUtils; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.util.Log; import android.util.Patterns; import android.view.View; import android.widget.TextView; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import com.nextcloud.android.sso.AccountImporter; import com.nextcloud.android.sso.api.NextcloudAPI; import com.nextcloud.android.sso.exceptions.AccountImportCancelledException; import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted; import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; import com.nextcloud.android.sso.helper.SingleAccountHelper; import com.nextcloud.android.sso.helper.VersionCheckHelper; import com.nextcloud.android.sso.model.FilesAppType; import com.nextcloud.android.sso.model.SingleSignOnAccount; import com.nextcloud.android.sso.ui.UiExceptionManager; import java.net.MalformedURLException; import java.net.URL; import javax.inject.Inject; import de.luhmer.owncloudnewsreader.database.DatabaseConnectionOrm; import de.luhmer.owncloudnewsreader.databinding.ActivityLoginDialogBinding; import de.luhmer.owncloudnewsreader.di.ApiProvider; import de.luhmer.owncloudnewsreader.model.NextcloudNewsVersion; import de.luhmer.owncloudnewsreader.ssl.MemorizingTrustManager; import de.luhmer.owncloudnewsreader.ssl.OkHttpSSLClient; import io.reactivex.Observer; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.annotations.NonNull; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; /** * Activity which displays a login screen to the user, offering registration as * well. */ public class LoginDialogActivity extends AppCompatActivity { private final String TAG = LoginDialogActivity.class.getCanonicalName(); public static final int RESULT_LOGIN = 16000; /** * Keep track of the login task to ensure we can cancel it if requested. */ protected @Inject ApiProvider mApi; protected @Inject SharedPreferences mPrefs; protected @Inject MemorizingTrustManager mMemorizingTrustManager; //private UserLoginTask mAuthTask = null; // Values for email and password at the time of the login attempt. private String mUsername; private String mPassword; private String mOc_root_path; // UI references. protected ActivityLoginDialogBinding binding; private SingleSignOnAccount importedAccount = null; private boolean mPasswordVisible = false; @Override public void onCreate(Bundle savedInstance) { super.onCreate(savedInstance); ((NewsReaderApplication) getApplication()).getAppComponent().injectActivity(this); binding = ActivityLoginDialogBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); binding.btnSingleSignOn.setOnClickListener((v) -> startSingleSignOn()); binding.btnLogin.setOnClickListener((v) -> startManualLogin()); binding.tvManualLogin.setOnClickListener((v) -> manualLogin()); // Manual Login binding.imgViewShowPassword.setOnClickListener(ImgViewShowPasswordListener); binding.password.addTextChangedListener(PasswordTextChangedListener); mUsername = mPrefs.getString(SettingsActivity.EDT_USERNAME_STRING, ""); mPassword = mPrefs.getString(SettingsActivity.EDT_PASSWORD_STRING, ""); mOc_root_path = mPrefs.getString(SettingsActivity.EDT_OWNCLOUDROOTPATH_STRING, ""); boolean mCbDisableHostnameVerification = mPrefs.getBoolean(SettingsActivity.CB_DISABLE_HOSTNAME_VERIFICATION_STRING, false); if(!mPassword.isEmpty()) { binding.imgViewShowPassword.setVisibility(View.GONE); } // Set up the login form. binding.username.setText(mUsername); binding.password.setText(mPassword); binding.edtOwncloudRootPath.setText(mOc_root_path); binding.cbAllowAllSSLCertificates.setChecked(mCbDisableHostnameVerification); binding.cbAllowAllSSLCertificates.setOnCheckedChangeListener((buttonView, isChecked) -> mPrefs.edit() .putBoolean(SettingsActivity.CB_DISABLE_HOSTNAME_VERIFICATION_STRING, isChecked) .commit()); } @Override public void onBackPressed() { if (mPrefs.getString(SettingsActivity.EDT_OWNCLOUDROOTPATH_STRING, null) == null) { // exit application if no account is set uo finishAffinity(); } else { // go back to previous activity super.onBackPressed(); } } @Override protected void onStart() { super.onStart(); mMemorizingTrustManager.bindDisplayActivity(this); } @Override protected void onStop() { mMemorizingTrustManager.unbindDisplayActivity(this); super.onStop(); } public void startSingleSignOn() { if (!VersionCheckHelper.verifyMinVersion(LoginDialogActivity.this, MIN_NEXTCLOUD_FILES_APP_VERSION_CODE, FilesAppType.PROD)) { // Dialog will be shown automatically return; } binding.oldLoginWrapper.setVisibility(View.GONE); try { AccountImporter.pickNewAccount(LoginDialogActivity.this); } catch (NextcloudFilesAppNotInstalledException e) { UiExceptionManager.showDialogForException(LoginDialogActivity.this, e); } catch (AndroidGetAccountsPermissionNotGranted e) { AccountImporter.requestAndroidAccountPermissionsAndPickAccount(this); } } public void startManualLogin() { attemptLogin(); } public void manualLogin() { binding.oldLoginWrapper.setVisibility(View.VISIBLE); } private final TextWatcher PasswordTextChangedListener = 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) { } @Override public void afterTextChanged(Editable s) { if(s.toString().isEmpty()) { binding.imgViewShowPassword.setVisibility(View.VISIBLE); } } }; private final View.OnClickListener ImgViewShowPasswordListener = new View.OnClickListener() { @Override public void onClick(View v) { int lastSelection = binding.password.getSelectionEnd(); mPasswordVisible = !mPasswordVisible; if(mPasswordVisible) { binding.password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); } else { binding.password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } binding.password.setSelection(lastSelection); } }; private ProgressDialog buildPendingDialogWhileLoggingIn() { ProgressDialog pDialog = new ProgressDialog(this); pDialog.setTitle(getString(R.string.login_progress_signing_in)); return pDialog; } private void loginSingleSignOn() { final ProgressDialog dialogLogin = buildPendingDialogWhileLoggingIn(); dialogLogin.show(); Editor editor = mPrefs.edit(); editor.putString(SettingsActivity.EDT_OWNCLOUDROOTPATH_STRING, importedAccount.url); editor.putString(SettingsActivity.EDT_PASSWORD_STRING, importedAccount.token); editor.putString(SettingsActivity.EDT_USERNAME_STRING, importedAccount.name); editor.putBoolean(SettingsActivity.SW_USE_SINGLE_SIGN_ON, true); editor.commit(); resetDatabase(); SingleAccountHelper.setCurrentAccount(this, importedAccount.name); mApi.initApi(new NextcloudAPI.ApiConnectedListener() { @Override public void onConnected() { Log.d(TAG, "onConnected() called"); finishLogin(dialogLogin); } @Override public void onError(Exception ex) { dialogLogin.dismiss(); Log.d(TAG, "onError() called with: ex = [" + ex + "]"); ShowAlertDialog(getString(R.string.login_dialog_title_error), ex.getMessage(), LoginDialogActivity.this); } }); } /** * Attempts to sign in or register the account specified by the login form. * If there are form errors (invalid email, missing fields, etc.), the * errors are presented and no actual login attempt is made. */ @SuppressLint({"SetTextI18n"}) public void attemptLogin() { // Reset errors. binding.username.setError(null); binding.password.setError(null); binding.edtOwncloudRootPath.setError(null); // Append "https://" is url doesn't contain it already mOc_root_path = requireNonNull(binding.edtOwncloudRootPath.getText()).toString().trim(); if(!mOc_root_path.startsWith("http")) { binding.edtOwncloudRootPath.setText("https://" + mOc_root_path); } // Store values at the time of the login attempt. mUsername = requireNonNull(binding.username.getText()).toString().trim(); mPassword = requireNonNull(binding.password.getText()).toString(); mOc_root_path = binding.edtOwncloudRootPath.getText().toString().trim(); boolean cancel = false; View focusView = null; // Check for a valid password. if (TextUtils.isEmpty(mPassword)) { binding.password.setError(getString(R.string.error_field_required)); focusView = binding.password; cancel = true; } // Check for a valid email address. if (TextUtils.isEmpty(mUsername)) { binding.username.setError(getString(R.string.error_field_required)); focusView = binding.username; cancel = true; } if (TextUtils.isEmpty(mOc_root_path)) { binding.edtOwncloudRootPath.setError(getString(R.string.error_field_required)); focusView = binding.edtOwncloudRootPath; cancel = true; } else { try { URL url = new URL(mOc_root_path); if(!Patterns.WEB_URL.matcher(mOc_root_path).matches()) { throw new MalformedURLException(); } if (!url.getProtocol().equals("https")) { ShowAlertDialog(getString(R.string.login_dialog_title_security_warning), getString(R.string.login_dialog_text_security_warning), this); } } catch (MalformedURLException e) { binding.edtOwncloudRootPath.setError(getString(R.string.error_invalid_url)); focusView = binding.edtOwncloudRootPath; cancel = true; } } if (cancel) { // There was an error; don't attempt login and focus the first // form field with an error. focusView.requestFocus(); } else { Editor editor = mPrefs.edit(); editor.putString(SettingsActivity.EDT_OWNCLOUDROOTPATH_STRING, mOc_root_path); editor.putString(SettingsActivity.EDT_PASSWORD_STRING, mPassword); editor.putString(SettingsActivity.EDT_USERNAME_STRING, mUsername); editor.putBoolean(SettingsActivity.SW_USE_SINGLE_SIGN_ON, false); editor.commit(); resetDatabase(); final ProgressDialog dialogLogin = buildPendingDialogWhileLoggingIn(); dialogLogin.show(); mApi.initApi(new NextcloudAPI.ApiConnectedListener() { @Override public void onConnected() { Log.d(TAG, "onConnected() called"); finishLogin(dialogLogin); } @Override public void onError(Exception ex) { dialogLogin.dismiss(); Log.d(TAG, "onError() called with: ex = [" + ex + "]"); ShowAlertDialog(getString(R.string.login_dialog_title_error), ex.getMessage(), LoginDialogActivity.this); } }); } } private void resetDatabase() { //Reset Database DatabaseConnectionOrm dbConn = new DatabaseConnectionOrm(LoginDialogActivity.this); dbConn.resetDatabase(); } private void finishLogin(final ProgressDialog dialogLogin) { mApi.getNewsAPI().version() .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer() { boolean loginSuccessful = false; @Override public void onSubscribe(@NonNull Disposable d) { Log.v(TAG, "onSubscribe() called with: d = [" + d + "]"); } @Override public void onNext(@NonNull NextcloudNewsVersion version) { Log.v(TAG, "onNext() called with: status = [" + version.version + "]"); loginSuccessful = true; mPrefs.edit().putString(Constants.NEWS_WEB_VERSION_NUMBER_STRING, version.version).apply(); if(version.version.equals("0")) { ShowAlertDialog(getString(R.string.login_dialog_title_error), getString(R.string.login_dialog_text_zero_version_code), LoginDialogActivity.this); loginSuccessful = false; } importedAccount = null; } @Override public void onError(@NonNull Throwable e) { dialogLogin.dismiss(); Log.v(TAG, "onError() called with: e = [" + e + "]"); Throwable t = OkHttpSSLClient.HandleExceptions(e); if(t instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) t).getStatusCode() == 302) { ShowAlertDialog( getString(R.string.login_dialog_title_error), getString(R.string.login_dialog_text_news_app_not_installed_on_server, "https://github.com/nextcloud/news/blob/master/docs/install.md#installing-from-the-app-store"), LoginDialogActivity.this); } else { ShowAlertDialog(getString(R.string.login_dialog_title_error), t.getMessage(), LoginDialogActivity.this); } } @Override public void onComplete() { dialogLogin.dismiss(); Log.v(TAG, "onComplete() called"); if(loginSuccessful) { Intent returnIntent = new Intent(); setResult(RESULT_OK, returnIntent); finish(); } } }); } public static void ShowAlertDialog(String title, String text, Activity activity) { // Linkify the message final SpannableString s = new SpannableString(text != null ? text : activity.getString(R.string.login_dialog_select_account_unknown_error_toast)); Linkify.addLinks(s, Linkify.ALL); AlertDialog aDialog = new AlertDialog.Builder(activity) .setTitle(title) .setMessage(s) .setPositiveButton(activity.getString(android.R.string.ok), null) .create(); aDialog.show(); // Make the textview clickable. Must be called after show() ((TextView) aDialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); try { AccountImporter.onActivityResult(requestCode, resultCode, data, LoginDialogActivity.this, account -> { LoginDialogActivity.this.importedAccount = account; loginSingleSignOn(); }); } catch (AccountImportCancelledException ignored) { } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this); } }