diff --git a/build.gradle b/build.gradle index be0b562778..0ef1ad7b45 100644 --- a/build.gradle +++ b/build.gradle @@ -91,6 +91,7 @@ android { testInstrumentationRunnerArgument "TEST_SERVER_URL", "\"$System.env.OCTEST_SERVER_BASE_URL\"" multiDexEnabled true + vectorDrawables.useSupportLibrary = true versionCode versionMajor * 10000000 + versionMinor * 10000 + versionPatch * 100 + versionBuild diff --git a/drawable_resources/first_run_files.svg b/drawable_resources/first_run_files.svg new file mode 100644 index 0000000000..cce515f084 --- /dev/null +++ b/drawable_resources/first_run_files.svg @@ -0,0 +1,56 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/drawable_resources/first_run_groupware.svg b/drawable_resources/first_run_groupware.svg new file mode 100644 index 0000000000..a653389300 --- /dev/null +++ b/drawable_resources/first_run_groupware.svg @@ -0,0 +1,56 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/drawable_resources/first_run_talk.svg b/drawable_resources/first_run_talk.svg new file mode 100644 index 0000000000..5c38b27b72 --- /dev/null +++ b/drawable_resources/first_run_talk.svg @@ -0,0 +1,66 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index b1f12155a7..be8ff7c521 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -254,6 +254,10 @@ android:configChanges="orientation|screenSize|keyboardHidden"/> + diff --git a/src/main/java/com/owncloud/android/MainApp.java b/src/main/java/com/owncloud/android/MainApp.java index 6c253cc529..6009d8a727 100644 --- a/src/main/java/com/owncloud/android/MainApp.java +++ b/src/main/java/com/owncloud/android/MainApp.java @@ -662,4 +662,9 @@ public class MainApp extends MultiDexApplication { } } } + + static public int getLastSeenVersionCode(Context context) { + SharedPreferences pref = android.preference.PreferenceManager.getDefaultSharedPreferences(context); + return pref.getInt(WhatsNewActivity.KEY_LAST_SEEN_VERSION_CODE, 0); + } } diff --git a/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java b/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java index 61738a9110..abe3842fb9 100644 --- a/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java +++ b/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java @@ -1,2288 +1,2307 @@ -/* - * ownCloud Android client application - * - * @author Bartek Przybylski - * @author David A. Velasco - * @author masensio - * @author Mario Danic - * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2015 ownCloud Inc. - * Copyright (C) 2017 Mario Danic - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * All changes by Mario Danic are distributed under the following terms: - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program 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 program. If not, see . - * - */ - -package com.owncloud.android.authentication; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.annotation.SuppressLint; -import android.app.Dialog; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.SharedPreferences; -import android.content.pm.ActivityInfo; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.net.http.SslError; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; -import android.support.design.widget.TextInputLayout; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; -import android.text.Editable; -import android.text.InputType; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.AndroidRuntimeException; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnFocusChangeListener; -import android.view.View.OnTouchListener; -import android.view.inputmethod.EditorInfo; -import android.webkit.CookieManager; -import android.webkit.CookieSyncManager; -import android.webkit.HttpAuthHandler; -import android.webkit.SslErrorHandler; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.TextView.OnEditorActionListener; - -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.authentication.SsoWebViewClient.SsoWebViewClientListener; -import com.owncloud.android.lib.common.OwnCloudAccount; -import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; -import com.owncloud.android.lib.common.OwnCloudCredentials; -import com.owncloud.android.lib.common.OwnCloudCredentialsFactory; -import com.owncloud.android.lib.common.UserInfo; -import com.owncloud.android.lib.common.accounts.AccountTypeUtils; -import com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException; -import com.owncloud.android.lib.common.accounts.AccountUtils.Constants; -import com.owncloud.android.lib.common.network.CertificateCombinedException; -import com.owncloud.android.lib.common.network.NetworkUtils; -import com.owncloud.android.lib.common.operations.OnRemoteOperationListener; -import com.owncloud.android.lib.common.operations.RemoteOperation; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.status.OwnCloudVersion; -import com.owncloud.android.lib.resources.users.GetRemoteUserInfoOperation; -import com.owncloud.android.operations.DetectAuthenticationMethodOperation.AuthenticationMethod; -import com.owncloud.android.operations.GetServerInfoOperation; -import com.owncloud.android.operations.OAuth2GetAccessToken; -import com.owncloud.android.services.OperationsService; -import com.owncloud.android.services.OperationsService.OperationsServiceBinder; -import com.owncloud.android.ui.components.CustomEditText; -import com.owncloud.android.ui.dialog.CredentialsDialogFragment; -import com.owncloud.android.ui.dialog.IndeterminateProgressDialog; -import com.owncloud.android.ui.dialog.SamlWebViewDialog; -import com.owncloud.android.ui.dialog.SslUntrustedCertDialog; -import com.owncloud.android.ui.dialog.SslUntrustedCertDialog.OnSslUntrustedCertListener; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.ErrorMessageAdapter; - -import java.io.InputStream; -import java.net.URLDecoder; -import java.security.cert.X509Certificate; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -/** - * This Activity is used to add an ownCloud account to the App - */ -public class AuthenticatorActivity extends AccountAuthenticatorActivity - implements OnRemoteOperationListener, OnFocusChangeListener, OnEditorActionListener, - SsoWebViewClientListener, OnSslUntrustedCertListener, - AuthenticatorAsyncTask.OnAuthenticatorTaskListener { - - private static final String TAG = AuthenticatorActivity.class.getSimpleName(); - - public static final String EXTRA_ACTION = "ACTION"; - public static final String EXTRA_ACCOUNT = "ACCOUNT"; - - private static final String KEY_AUTH_TOKEN_TYPE = "AUTH_TOKEN_TYPE"; - - private static final String KEY_HOST_URL_TEXT = "HOST_URL_TEXT"; - private static final String KEY_OC_VERSION = "OC_VERSION"; - private static final String KEY_SERVER_VALID = "SERVER_VALID"; - private static final String KEY_SERVER_CHECKED = "SERVER_CHECKED"; - private static final String KEY_SERVER_STATUS_TEXT = "SERVER_STATUS_TEXT"; - private static final String KEY_SERVER_STATUS_ICON = "SERVER_STATUS_ICON"; - private static final String KEY_IS_SSL_CONN = "IS_SSL_CONN"; - private static final String KEY_PASSWORD_EXPOSED = "PASSWORD_VISIBLE"; - private static final String KEY_AUTH_STATUS_TEXT = "AUTH_STATUS_TEXT"; - private static final String KEY_AUTH_STATUS_ICON = "AUTH_STATUS_ICON"; - private static final String KEY_SERVER_AUTH_METHOD = "SERVER_AUTH_METHOD"; - private static final String KEY_WAITING_FOR_OP_ID = "WAITING_FOR_OP_ID"; - private static final String KEY_AUTH_TOKEN = "AUTH_TOKEN"; - - private static final String AUTH_ON = "on"; - private static final String AUTH_OPTIONAL = "optional"; - - public static final byte ACTION_CREATE = 0; - public static final byte ACTION_UPDATE_EXPIRED_TOKEN = 2; // detected by the app - - private static final String UNTRUSTED_CERT_DIALOG_TAG = "UNTRUSTED_CERT_DIALOG"; - private static final String SAML_DIALOG_TAG = "SAML_DIALOG"; - private static final String WAIT_DIALOG_TAG = "WAIT_DIALOG"; - private static final String CREDENTIALS_DIALOG_TAG = "CREDENTIALS_DIALOG"; - private static final String KEY_AUTH_IS_FIRST_ATTEMPT_TAG = "KEY_AUTH_IS_FIRST_ATTEMPT"; - - private static final String KEY_USERNAME = "USERNAME"; - private static final String KEY_PASSWORD = "PASSWORD"; - private static final String KEY_ASYNC_TASK_IN_PROGRESS = "AUTH_IN_PROGRESS"; - private static final String WEB_LOGIN = "/index.php/login/flow"; - public static final String PROTOCOL_SUFFIX = "://"; - public static final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"; - public static final String HTTPS_PROTOCOL = "https://"; - public static final String HTTP_PROTOCOL = "http://"; - - public static final String REGULAR_SERVER_INPUT_TYPE = "regular"; - public static final String SUBDOMAIN_SERVER_INPUT_TYPE = "prefix"; - public static final String DIRECTORY_SERVER_INPUT_TYPE = "suffix"; - - /// parameters from EXTRAs in starter Intent - private byte mAction; - private Account mAccount; - private String mAuthTokenType; - - /// activity-level references / state - private final Handler mHandler = new Handler(); - private ServiceConnection mOperationsServiceConnection = null; - private OperationsServiceBinder mOperationsServiceBinder = null; - private AccountManager mAccountMgr; - private Uri mNewCapturedUriFromOAuth2Redirection; - - /// Server PRE-Fragment elements - private CustomEditText mHostUrlInput; - private View mRefreshButton; - private TextView mServerStatusView; - - private TextWatcher mHostUrlInputWatcher; - private String mServerStatusText = ""; - private int mServerStatusIcon; - - private boolean mServerIsChecked; - private boolean mServerIsValid; - - private GetServerInfoOperation.ServerInfo mServerInfo = new GetServerInfoOperation.ServerInfo(); - - /// Authentication PRE-Fragment elements - private CheckBox mOAuth2Check; - private TextView mOAuthAuthEndpointText; - private TextView mOAuthTokenEndpointText; - private EditText mUsernameInput; - private EditText mPasswordInput; - private View mOkButton; - private TextView mAuthStatusView; - private ImageButton mTestServerButton; - - private WebView mLoginWebView; - - private String mAuthStatusText = ""; - private int mAuthStatusIcon; - - private String mAuthToken = ""; - private AuthenticatorAsyncTask mAsyncTask; - - private boolean mIsFirstAuthAttempt; - - /// Identifier of operation in progress which result shouldn't be lost - private long mWaitingForOpId = Long.MAX_VALUE; - - private String basicTokenType; - private String oauthTokenType; - private String samlTokenType; - - private boolean webViewLoginMethod; - private String webViewUser; - private String webViewPassword; - private TextInputLayout mUsernameInputLayout; - private TextInputLayout mPasswordInputLayout; - private boolean forceOldLoginMethod; - - /** - * {@inheritDoc} - * - * IMPORTANT ENTRY POINT 1: activity is shown to the user - */ - @Override - protected void onCreate(Bundle savedInstanceState) { - //Log_OC.e(TAG, "onCreate init"); - super.onCreate(savedInstanceState); - - basicTokenType = AccountTypeUtils.getAuthTokenTypePass(MainApp.getAccountType(this)); - oauthTokenType = AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType(this)); - samlTokenType = AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType(this)); - - // delete cookies for webView - deleteCookies(); - - // Workaround, for fixing a problem with Android Library Suppor v7 19 - //getWindow().requestFeature(Window.FEATURE_NO_TITLE); - if (getSupportActionBar() != null) { - getSupportActionBar().hide(); - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - getSupportActionBar().setDisplayShowHomeEnabled(false); - getSupportActionBar().setDisplayShowTitleEnabled(false); - } - - mIsFirstAuthAttempt = true; - - /// init activity state - mAccountMgr = AccountManager.get(this); - mNewCapturedUriFromOAuth2Redirection = null; - - /// get input values - mAction = getIntent().getByteExtra(EXTRA_ACTION, ACTION_CREATE); - mAccount = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT); - if (savedInstanceState == null) { - initAuthTokenType(); - } else { - mAuthTokenType = savedInstanceState.getString(KEY_AUTH_TOKEN_TYPE); - mWaitingForOpId = savedInstanceState.getLong(KEY_WAITING_FOR_OP_ID); - mIsFirstAuthAttempt = savedInstanceState.getBoolean(KEY_AUTH_IS_FIRST_ATTEMPT_TAG); - } - - webViewLoginMethod = !TextUtils.isEmpty(getResources().getString(R.string.webview_login_url)); - - if (webViewLoginMethod) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - } - - /// load user interface - if (!webViewLoginMethod) { - setContentView(R.layout.account_setup); - - /// initialize general UI elements - initOverallUi(); - - findViewById(R.id.centeredRefreshButton).setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - checkOcServer(); - } - }); - - findViewById(R.id.embeddedRefreshButton).setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - checkOcServer(); - } - }); - - /// initialize block to be moved to single Fragment to check server and get info about it - - /// initialize block to be moved to single Fragment to retrieve and validate credentials - initAuthorizationPreFragment(savedInstanceState); - - } else { - setContentView(R.layout.account_setup_webview); - mLoginWebView = findViewById(R.id.login_webview); - initWebViewLogin(null); - } - - initServerPreFragment(savedInstanceState); - } - - private void deleteCookies() { - try { - CookieSyncManager.createInstance(this); - CookieManager cookieManager = CookieManager.getInstance(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies(null); - } else { - cookieManager.removeAllCookie(); - } - } catch (AndroidRuntimeException e) { - Log_OC.e(TAG, e.getMessage()); - } - } - - private static String getWebLoginUserAgent() { - return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) + - Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL; - } - - @SuppressLint("SetJavaScriptEnabled") - private void initWebViewLogin(String baseURL) { - mLoginWebView.setVisibility(View.GONE); - - final ProgressBar progressBar = findViewById(R.id.login_webview_progress_bar); - - mLoginWebView.getSettings().setAllowFileAccess(false); - mLoginWebView.getSettings().setJavaScriptEnabled(true); - mLoginWebView.getSettings().setDomStorageEnabled(true); - mLoginWebView.getSettings().setUserAgentString(getWebLoginUserAgent()); - mLoginWebView.getSettings().setSaveFormData(false); - mLoginWebView.getSettings().setSavePassword(false); - - Map headers = new HashMap<>(); - headers.put(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE); - - String url; - if (baseURL != null && !baseURL.isEmpty()) { - url = baseURL + WEB_LOGIN; - } else { - url = getResources().getString(R.string.webview_login_url); - } - - mLoginWebView.loadUrl(url, headers); - - setClient(progressBar); - - // show snackbar after 60s to switch back to old login method - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - DisplayUtils.createSnackbar(mLoginWebView, R.string.fallback_weblogin_text, Snackbar.LENGTH_INDEFINITE) - .setActionTextColor(getResources().getColor(R.color.primary_dark)) - .setAction(R.string.fallback_weblogin_back, new View.OnClickListener() { - @Override - public void onClick(View v) { - mLoginWebView.setVisibility(View.INVISIBLE); - webViewLoginMethod = false; - - setContentView(R.layout.account_setup); - - // initialize general UI elements - initOverallUi(); - - mPasswordInputLayout.setVisibility(View.VISIBLE); - mUsernameInputLayout.setVisibility(View.VISIBLE); - mUsernameInput.requestFocus(); - mOAuth2Check.setVisibility(View.INVISIBLE); - mAuthStatusView.setVisibility(View.INVISIBLE); - mServerStatusView.setVisibility(View.INVISIBLE); - mTestServerButton.setVisibility(View.INVISIBLE); - forceOldLoginMethod = true; - mOkButton.setVisibility(View.VISIBLE); - - initServerPreFragment(null); - - mHostUrlInput.setText(baseURL); - - checkOcServer(); - } - }).show(); - } - }, 60000); - } - - private void setClient(ProgressBar progressBar) { - mLoginWebView.setWebViewClient(new WebViewClient() { - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url.startsWith(getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/")) { - parseAndLoginFromWebView(url); - return true; - } - return false; - } - - @Override - public void onPageFinished(WebView view, String url) { - super.onPageFinished(view, url); - - progressBar.setVisibility(View.GONE); - mLoginWebView.setVisibility(View.VISIBLE); - } - - @Override - public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - X509Certificate cert = SsoWebViewClient.getX509CertificateFromError(error); - - try { - if (cert != null && NetworkUtils.isCertInKnownServersStore(cert, getApplicationContext())) { - handler.proceed(); - } else { - showUntrustedCertDialog(cert, error, handler); - } - } catch (Exception e) { - Log_OC.e(TAG, "Cert could not be verified"); - } - } - - public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { - progressBar.setVisibility(View.GONE); - mLoginWebView.setVisibility(View.VISIBLE); - - InputStream resources = getResources().openRawResource(R.raw.custom_error); - String customError = DisplayUtils.getData(resources); - - if (!customError.isEmpty()) { - mLoginWebView.loadData(customError, "text/html; charset=UTF-8", null); - } - } - }); - } - - private void parseAndLoginFromWebView(String dataString) { - String prefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/"; - LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, dataString); - - if (loginUrlInfo != null) { - mServerInfo.mBaseUrl = AuthenticatorUrlUtils.normalizeUrlSuffix(loginUrlInfo.serverAddress); - webViewUser = loginUrlInfo.username; - webViewPassword = loginUrlInfo.password; - checkOcServer(); - } - } - - private void populateLoginFields(String dataString) throws IllegalArgumentException { - // check if it is cloud://login/ - if (dataString.startsWith(getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/")) { - String prefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/"; - LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, dataString); - - if (loginUrlInfo != null) { - mHostUrlInput.setText(loginUrlInfo.serverAddress); - mUsernameInput.setText(loginUrlInfo.username); - mPasswordInput.setText(loginUrlInfo.password); - - if (loginUrlInfo.serverAddress != null && !mServerIsChecked) { - onUrlInputFocusLost(); - } - } - } - } - - /** - * parses a URI string and returns a login data object with the information from the URI string. - * - * @param prefix URI beginning, e.g. cloud://login/ - * @param dataString the complete URI - * @return login data - * @throws IllegalArgumentException when - */ - public static LoginUrlInfo parseLoginDataUrl(String prefix, String dataString) throws IllegalArgumentException { - if (dataString.length() < prefix.length()) { - throw new IllegalArgumentException("Invalid login URL detected"); - } - LoginUrlInfo loginUrlInfo = new LoginUrlInfo(); - - // format is basically xxx://login/server:xxx&user:xxx&password while all variables are optional - String data = dataString.substring(prefix.length()); - - // parse data - String[] values = data.split("&"); - - if (values.length < 1 || values.length > 3) { - // error illegal number of URL elements detected - throw new IllegalArgumentException("Illegal number of login URL elements detected: " + values.length); - } - - for (String value : values) { - if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { - loginUrlInfo.username = URLDecoder.decode( - value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())); - } else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { - loginUrlInfo.password = URLDecoder.decode( - value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())); - } else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { - loginUrlInfo.serverAddress = URLDecoder.decode( - value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())); - } else { - // error illegal URL element detected - throw new IllegalArgumentException("Illegal magic login URL element detected: " + value); - } - } - - return loginUrlInfo; - } - - private void initAuthTokenType() { - mAuthTokenType = getIntent().getExtras().getString(AccountAuthenticator.KEY_AUTH_TOKEN_TYPE); - if (mAuthTokenType == null) { - if (mAccount != null) { - boolean oAuthRequired = mAccountMgr.getUserData(mAccount, Constants.KEY_SUPPORTS_OAUTH2) != null; - boolean samlWebSsoRequired = ( - mAccountMgr.getUserData - (mAccount, Constants.KEY_SUPPORTS_SAML_WEB_SSO) != null - ); - mAuthTokenType = chooseAuthTokenType(oAuthRequired, samlWebSsoRequired); - - } else { - boolean oAuthSupported = AUTH_ON.equals(getString(R.string.auth_method_oauth2)); - boolean samlWebSsoSupported = AUTH_ON.equals(getString(R.string.auth_method_saml_web_sso)); - mAuthTokenType = chooseAuthTokenType(oAuthSupported, samlWebSsoSupported); - } - } - } - - private String chooseAuthTokenType(boolean oauth, boolean saml) { - if (saml) { - return samlTokenType; - } else if (oauth) { - return oauthTokenType; - } else { - return basicTokenType; - } - } - - - /** - * Configures elements in the user interface under direct control of the Activity. - */ - private void initOverallUi() { - mHostUrlInput = findViewById(R.id.hostUrlInput); - mUsernameInputLayout = findViewById(R.id.input_layout_account_username); - mPasswordInputLayout = findViewById(R.id.input_layout_account_password); - mPasswordInput = findViewById(R.id.account_password); - mUsernameInput = findViewById(R.id.account_username); - mAuthStatusView = findViewById(R.id.auth_status_text); - mOAuth2Check = findViewById(R.id.oauth_onOff_check); - mServerStatusView = findViewById(R.id.server_status_text); - mTestServerButton = findViewById(R.id.testServerButton); - - mOkButton = findViewById(R.id.buttonOK); - mOkButton.setOnClickListener(v -> onOkClick()); - - setupWelcomeLink(); - setupInstructionMessage(); - - mTestServerButton.setVisibility(mAction == ACTION_CREATE ? View.VISIBLE : View.GONE); - } - - private void setupWelcomeLink() { - Button welcomeLink = findViewById(R.id.welcome_link); - welcomeLink.setVisibility(mAction == ACTION_CREATE && - getResources().getBoolean(R.bool.show_welcome_link) ? View.VISIBLE : View.GONE); - welcomeLink.setText(getString(R.string.auth_register)); - } - - private void setupInstructionMessage() { - String instructionsMessageText = calculateInstructionMessageText(mAction, mAuthTokenType); - TextView instructionsView = findViewById(R.id.instructions_message); - - if (instructionsMessageText != null) { - instructionsView.setVisibility(View.VISIBLE); - instructionsView.setText(instructionsMessageText); - } else { - instructionsView.setVisibility(View.GONE); - } - } - - @Nullable - private String calculateInstructionMessageText(byte action, String authTokenType) { - if (action == ACTION_UPDATE_EXPIRED_TOKEN) { - if (AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType(this)).equals(authTokenType)) { - return getString(R.string.auth_expired_oauth_token_toast); - - } else if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType(this)) - .equals(authTokenType)) { - return getString(R.string.auth_expired_saml_sso_token_toast); - - } else { - return getString(R.string.auth_expired_basic_auth_toast); - } - } - - return null; - } - - public void onTestServerConnectionClick(View v) { - checkOcServer(); - } - - - /** - * @param savedInstanceState Saved activity state, as in {{@link #onCreate(Bundle)} - */ - private void initServerPreFragment(Bundle savedInstanceState) { - - /// step 1 - load and process relevant inputs (resources, intent, savedInstanceState) - boolean isUrlInputAllowed = getResources().getBoolean(R.bool.show_server_url_input); - if (savedInstanceState == null) { - if (mAccount != null) { - mServerInfo.mBaseUrl = mAccountMgr.getUserData(mAccount, Constants.KEY_OC_BASE_URL); - // TODO do next in a setter for mBaseUrl - mServerInfo.mIsSslConn = mServerInfo.mBaseUrl.startsWith(HTTPS_PROTOCOL); - mServerInfo.mVersion = AccountUtils.getServerVersion(mAccount); - } else { - if (!webViewLoginMethod) { - mServerInfo.mBaseUrl = getString(R.string.server_url).trim(); - } else { - mServerInfo.mBaseUrl = getString(R.string.webview_login_url).trim(); - } - mServerInfo.mIsSslConn = mServerInfo.mBaseUrl.startsWith(HTTPS_PROTOCOL); - } - } else { - mServerStatusText = savedInstanceState.getString(KEY_SERVER_STATUS_TEXT); - mServerStatusIcon = savedInstanceState.getInt(KEY_SERVER_STATUS_ICON); - - mServerIsValid = savedInstanceState.getBoolean(KEY_SERVER_VALID); - mServerIsChecked = savedInstanceState.getBoolean(KEY_SERVER_CHECKED); - - // TODO parcelable - mServerInfo.mIsSslConn = savedInstanceState.getBoolean(KEY_IS_SSL_CONN); - mServerInfo.mBaseUrl = savedInstanceState.getString(KEY_HOST_URL_TEXT); - String ocVersion = savedInstanceState.getString(KEY_OC_VERSION); - if (ocVersion != null) { - mServerInfo.mVersion = new OwnCloudVersion(ocVersion); - } - mServerInfo.mAuthMethod = AuthenticationMethod.valueOf( - savedInstanceState.getString(KEY_SERVER_AUTH_METHOD)); - - } - - if (!webViewLoginMethod) { - /// step 2 - set properties of UI elements (text, visibility, enabled...) - mHostUrlInput = findViewById(R.id.hostUrlInput); - // Convert IDN to Unicode - mHostUrlInput.setText(DisplayUtils.convertIdn(mServerInfo.mBaseUrl, false)); - if (mAction != ACTION_CREATE) { - /// lock things that should not change - mHostUrlInput.setEnabled(false); - mHostUrlInput.setFocusable(false); - } - if (isUrlInputAllowed) { - mRefreshButton = findViewById(R.id.embeddedRefreshButton); - } else { - findViewById(R.id.hostUrlFrame).setVisibility(View.GONE); - mRefreshButton = findViewById(R.id.centeredRefreshButton); - } - showRefreshButton(mServerIsChecked && !mServerIsValid && - mWaitingForOpId > Integer.MAX_VALUE); - mServerStatusView = findViewById(R.id.server_status_text); - showServerStatus(); - - /// step 3 - bind some listeners and options - mHostUrlInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); - mHostUrlInput.setOnEditorActionListener(this); - - /// step 4 - create listeners that will be bound at onResume - mHostUrlInputWatcher = new TextWatcher() { - - @Override - public void afterTextChanged(Editable s) { - if (mOkButton.isEnabled() && - !mServerInfo.mBaseUrl.equals( - AuthenticatorUrlUtils.normalizeUrl(s.toString(), mServerInfo.mIsSslConn))) { - mOkButton.setEnabled(false); - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // not used at the moment - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - if (mAuthStatusIcon != 0) { - Log_OC.d(TAG, "onTextChanged: hiding authentication status"); - mAuthStatusIcon = 0; - mAuthStatusText = ""; - showAuthStatus(); - } - } - }; - - - // TODO find out if this is really necessary, or if it can done in a different way - findViewById(R.id.scroll).setOnTouchListener(new OnTouchListener() { - @Override - public boolean onTouch(View view, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN && - AccountTypeUtils.getAuthTokenTypeSamlSessionCookie( - MainApp.getAccountType(getBaseContext())).equals(mAuthTokenType) && - mHostUrlInput.hasFocus()) { - checkOcServer(); - } - return false; - } - }); - } - } - - - /** - * @param savedInstanceState Saved activity state, as in {{@link #onCreate(Bundle)} - */ - private void initAuthorizationPreFragment(Bundle savedInstanceState) { - - /// step 0 - get UI elements in layout - mOAuth2Check = findViewById(R.id.oauth_onOff_check); - mOAuthAuthEndpointText = findViewById(R.id.oAuthEntryPoint_1); - mOAuthTokenEndpointText = findViewById(R.id.oAuthEntryPoint_2); - mUsernameInput = findViewById(R.id.account_username); - mPasswordInput = findViewById(R.id.account_password); - mAuthStatusView = findViewById(R.id.auth_status_text); - - /// step 1 - load and process relevant inputs (resources, intent, savedInstanceState) - String presetUserName = null; - boolean isPasswordExposed = false; - if (savedInstanceState == null) { - if (mAccount != null) { - presetUserName = com.owncloud.android.lib.common.accounts.AccountUtils.getUsernameForAccount(mAccount); - } - } else { - isPasswordExposed = savedInstanceState.getBoolean(KEY_PASSWORD_EXPOSED, false); - mAuthStatusText = savedInstanceState.getString(KEY_AUTH_STATUS_TEXT); - mAuthStatusIcon = savedInstanceState.getInt(KEY_AUTH_STATUS_ICON); - mAuthToken = savedInstanceState.getString(KEY_AUTH_TOKEN); - } - - /// step 2 - set properties of UI elements (text, visibility, enabled...) - mOAuth2Check.setChecked( - AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType(this)).equals(mAuthTokenType)); - if (presetUserName != null) { - mUsernameInput.setText(presetUserName); - } - if (mAction != ACTION_CREATE) { - mUsernameInput.setEnabled(false); - mUsernameInput.setFocusable(false); - } - mPasswordInput.setText(""); // clean password to avoid social hacking - if (isPasswordExposed) { - showPassword(); - } - updateAuthenticationPreFragmentVisibility(); - showAuthStatus(); - mOkButton.setEnabled(mServerIsValid); - - - /// step 3 - bind listeners - // bindings for password input field - mPasswordInput.setOnFocusChangeListener(this); - mPasswordInput.setImeOptions(EditorInfo.IME_ACTION_DONE); - mPasswordInput.setOnEditorActionListener(this); - mPasswordInput.setOnTouchListener(new RightDrawableOnTouchListener() { - @Override - public boolean onDrawableTouch(final MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_UP) { - AuthenticatorActivity.this.onViewPasswordClick(); - } - return true; - } - }); - - } - - - /** - * Changes the visibility of input elements depending on - * the current authorization method. - */ - private void updateAuthenticationPreFragmentVisibility() { - if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType(this)).equals(mAuthTokenType)) { - // SAML-based web Single Sign On - mOAuth2Check.setVisibility(View.GONE); - mOAuthAuthEndpointText.setVisibility(View.GONE); - mOAuthTokenEndpointText.setVisibility(View.GONE); - mUsernameInput.setVisibility(View.GONE); - mPasswordInput.setVisibility(View.GONE); - - } else { - if (mAction == ACTION_CREATE && - AUTH_OPTIONAL.equals(getString(R.string.auth_method_oauth2))) { - mOAuth2Check.setVisibility(View.VISIBLE); - } else { - mOAuth2Check.setVisibility(View.GONE); - } - - if (AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType(this)).equals(mAuthTokenType)) { - // OAuth 2 authorization - mOAuthAuthEndpointText.setVisibility(View.VISIBLE); - mOAuthTokenEndpointText.setVisibility(View.VISIBLE); - mUsernameInput.setVisibility(View.GONE); - mPasswordInput.setVisibility(View.GONE); - - } else { - // basic HTTP authorization - mOAuthAuthEndpointText.setVisibility(View.GONE); - mOAuthTokenEndpointText.setVisibility(View.GONE); - mUsernameInput.setVisibility(View.VISIBLE); - mPasswordInput.setVisibility(View.VISIBLE); - } - } - } - - - /** - * Saves relevant state before {@link #onPause()} - * - * Do NOT save {@link #mNewCapturedUriFromOAuth2Redirection}; it keeps a temporal flag, - * intended to defer the processing of the redirection caught in - * {@link #onNewIntent(Intent)} until {@link #onResume()} - * - * See {@link super#onSaveInstanceState(Bundle)} - */ - @Override - protected void onSaveInstanceState(Bundle outState) { - //Log_OC.e(TAG, "onSaveInstanceState init" ); - super.onSaveInstanceState(outState); - - /// global state - outState.putString(KEY_AUTH_TOKEN_TYPE, mAuthTokenType); - outState.putLong(KEY_WAITING_FOR_OP_ID, mWaitingForOpId); - - if (!webViewLoginMethod) { - /// Server PRE-fragment state - outState.putString(KEY_SERVER_STATUS_TEXT, mServerStatusText); - outState.putInt(KEY_SERVER_STATUS_ICON, mServerStatusIcon); - outState.putBoolean(KEY_SERVER_CHECKED, mServerIsChecked); - outState.putBoolean(KEY_SERVER_VALID, mServerIsValid); - - /// Authentication PRE-fragment state - outState.putBoolean(KEY_PASSWORD_EXPOSED, isPasswordVisible()); - outState.putInt(KEY_AUTH_STATUS_ICON, mAuthStatusIcon); - outState.putString(KEY_AUTH_STATUS_TEXT, mAuthStatusText); - outState.putString(KEY_AUTH_TOKEN, mAuthToken); - } - - outState.putBoolean(KEY_IS_SSL_CONN, mServerInfo.mIsSslConn); - outState.putString(KEY_HOST_URL_TEXT, mServerInfo.mBaseUrl); - if (mServerInfo.mVersion != null) { - outState.putString(KEY_OC_VERSION, mServerInfo.mVersion.getVersion()); - } - outState.putString(KEY_SERVER_AUTH_METHOD, mServerInfo.mAuthMethod.name()); - - /// authentication - outState.putBoolean(KEY_AUTH_IS_FIRST_ATTEMPT_TAG, mIsFirstAuthAttempt); - - /// AsyncTask (User and password) - if (!webViewLoginMethod) { - outState.putString(KEY_USERNAME, mUsernameInput.getText().toString().trim()); - outState.putString(KEY_PASSWORD, mPasswordInput.getText().toString()); - } - - if (mAsyncTask != null) { - mAsyncTask.cancel(true); - outState.putBoolean(KEY_ASYNC_TASK_IN_PROGRESS, true); - } else { - outState.putBoolean(KEY_ASYNC_TASK_IN_PROGRESS, false); - } - mAsyncTask = null; - - //Log_OC.e(TAG, "onSaveInstanceState end" ); - } - - @Override - public void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - - mServerIsChecked = savedInstanceState.getBoolean(KEY_SERVER_CHECKED, false); - - // AsyncTask - boolean inProgress = savedInstanceState.getBoolean(KEY_ASYNC_TASK_IN_PROGRESS); - if (inProgress) { - String username = savedInstanceState.getString(KEY_USERNAME); - String password = savedInstanceState.getString(KEY_PASSWORD); - - OwnCloudCredentials credentials = null; - if (basicTokenType.equals(mAuthTokenType)) { - credentials = OwnCloudCredentialsFactory.newBasicCredentials(username, password); - - } else if (oauthTokenType.equals(mAuthTokenType)) { - credentials = OwnCloudCredentialsFactory.newBearerCredentials(mAuthToken); - - } - accessRootFolder(credentials); - } - } - - /** - * The redirection triggered by the OAuth authentication server as response to the - * GET AUTHORIZATION request is caught here. - * - * To make this possible, this activity needs to be qualified with android:launchMode = - * "singleTask" in the AndroidManifest.xml file. - */ - @Override - protected void onNewIntent(Intent intent) { - Log_OC.d(TAG, "onNewIntent()"); - Uri data = intent.getData(); - if (data != null && data.toString().startsWith(getString(R.string.oauth2_redirect_uri))) { - mNewCapturedUriFromOAuth2Redirection = data; - } - } - - - /** - * The redirection triggered by the OAuth authentication server as response to the - * GET AUTHORIZATION, and deferred in {@link #onNewIntent(Intent)}, is processed here. - */ - @Override - protected void onResume() { - super.onResume(); - - if (!webViewLoginMethod) { - // bound here to avoid spurious changes triggered by Android on device rotations - mHostUrlInput.setOnFocusChangeListener(this); - mHostUrlInput.addTextChangedListener(mHostUrlInputWatcher); - - if (mNewCapturedUriFromOAuth2Redirection != null) { - getOAuth2AccessTokenFromCapturedRedirection(); - } - - String dataString = getIntent().getDataString(); - if (dataString != null) { - try { - populateLoginFields(dataString); - } catch (IllegalArgumentException e) { - DisplayUtils.showSnackMessage(findViewById(R.id.scroll), R.string.auth_illegal_login_used); - Log_OC.e(TAG, "Illegal login data URL used, no Login pre-fill!", e); - } - } - } - - // bind to Operations Service - mOperationsServiceConnection = new OperationsServiceConnection(); - if (!bindService(new Intent(this, OperationsService.class), - mOperationsServiceConnection, - Context.BIND_AUTO_CREATE)) { - DisplayUtils.showSnackMessage(findViewById(R.id.scroll), R.string.error_cant_bind_to_operations_service); - finish(); - } - - if (mOperationsServiceBinder != null) { - doOnResumeAndBound(); - } - } - - - @Override - protected void onPause() { - if (mOperationsServiceBinder != null) { - mOperationsServiceBinder.removeOperationListener(this); - } - - if (!webViewLoginMethod) { - mHostUrlInput.removeTextChangedListener(mHostUrlInputWatcher); - mHostUrlInput.setOnFocusChangeListener(null); - } - - super.onPause(); - } - - @Override - protected void onDestroy() { - - mHostUrlInputWatcher = null; - - if (mOperationsServiceConnection != null) { - unbindService(mOperationsServiceConnection); - mOperationsServiceBinder = null; - } - - if (webViewLoginMethod) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR); - } - - super.onDestroy(); - } - - /** - * Parses the redirection with the response to the GET AUTHORIZATION request to the - * oAuth server and requests for the access token (GET ACCESS TOKEN) - */ - private void getOAuth2AccessTokenFromCapturedRedirection() { - /// Parse data from OAuth redirection - String queryParameters = mNewCapturedUriFromOAuth2Redirection.getQuery(); - mNewCapturedUriFromOAuth2Redirection = null; - - /// Showing the dialog with instructions for the user. - IndeterminateProgressDialog dialog = - IndeterminateProgressDialog.newInstance(R.string.auth_getting_authorization, true); - dialog.show(getSupportFragmentManager(), WAIT_DIALOG_TAG); - - /// GET ACCESS TOKEN to the oAuth server - Intent getServerInfoIntent = new Intent(); - getServerInfoIntent.setAction(OperationsService.ACTION_OAUTH2_GET_ACCESS_TOKEN); - - getServerInfoIntent.putExtra( - OperationsService.EXTRA_SERVER_URL, - mOAuthTokenEndpointText.getText().toString().trim()); - - getServerInfoIntent.putExtra( - OperationsService.EXTRA_OAUTH2_QUERY_PARAMETERS, - queryParameters); - - if (mOperationsServiceBinder != null) { - //Log_OC.e(TAG, "getting access token..." ); - mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getServerInfoIntent); - } - } - - - /** - * Handles the change of focus on the text inputs for the server URL and the password - */ - public void onFocusChange(View view, boolean hasFocus) { - if (view.getId() == R.id.hostUrlInput) { - if (!hasFocus) { - onUrlInputFocusLost(); - } else { - showRefreshButton(false); - } - - } else if (view.getId() == R.id.account_password) { - onPasswordFocusChanged(hasFocus); - } - } - - - /** - * Handles changes in focus on the text input for the server URL. - * - * IMPORTANT ENTRY POINT 2: When (!hasFocus), user wrote the server URL and changed to - * other field. The operation to check the existence of the server in the entered URL is - * started. - * - * When hasFocus: user 'comes back' to write again the server URL. - */ - private void onUrlInputFocusLost() { - if (!mServerInfo.mBaseUrl.equals( - AuthenticatorUrlUtils.normalizeUrl(mHostUrlInput.getText().toString(), mServerInfo.mIsSslConn))) { - // check server again only if the user changed something in the field - checkOcServer(); - } else { - mOkButton.setEnabled(mServerIsValid); - showRefreshButton(!mServerIsValid); - } - } - - - private void checkOcServer() { - String uri; - if (mHostUrlInput != null) { - uri = mHostUrlInput.getText().toString().trim(); - mOkButton.setEnabled(false); - showRefreshButton(false); - } else { - uri = mServerInfo.mBaseUrl; - } - - mServerIsValid = false; - mServerIsChecked = false; - mServerInfo = new GetServerInfoOperation.ServerInfo(); - - if (uri.length() != 0) { - if (mHostUrlInput != null) { - uri = AuthenticatorUrlUtils.stripIndexPhpOrAppsFiles(uri); - mHostUrlInput.setText(uri); - } - - // Handle internationalized domain names - try { - uri = DisplayUtils.convertIdn(uri, true); - } catch (IllegalArgumentException ex) { - // Let Owncloud library check the error of the malformed URI - Log_OC.e(TAG, "Error converting internationalized domain name " + uri, ex); - } - - if (mHostUrlInput != null) { - mServerStatusText = getResources().getString(R.string.auth_testing_connection); - mServerStatusIcon = R.drawable.progress_small; - showServerStatus(); - } - - Intent getServerInfoIntent = new Intent(); - getServerInfoIntent.setAction(OperationsService.ACTION_GET_SERVER_INFO); - getServerInfoIntent.putExtra(OperationsService.EXTRA_SERVER_URL, - AuthenticatorUrlUtils.normalizeUrlSuffix(uri)); - - if (mOperationsServiceBinder != null) { - mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getServerInfoIntent); - } else { - Log_OC.e(TAG, "Server check tried with OperationService unbound!"); - } - - } else { - mServerStatusText = ""; - mServerStatusIcon = 0; - if (!webViewLoginMethod) { - showServerStatus(); - } - } - } - - - /** - * Handles changes in focus on the text input for the password (basic authorization). - * - * When (hasFocus), the button to toggle password visibility is shown. - * - * When (!hasFocus), the button is made invisible and the password is hidden. - * - * @param hasFocus 'True' if focus is received, 'false' if is lost - */ - - private void onPasswordFocusChanged(boolean hasFocus) { - if (hasFocus) { - showViewPasswordButton(); - } else { - hidePassword(); - hidePasswordButton(); - } - } - - - private void showViewPasswordButton() { - int drawable = R.drawable.ic_view; - if (isPasswordVisible()) { - drawable = R.drawable.ic_hide; - } - mPasswordInput.setCompoundDrawablesWithIntrinsicBounds(0, 0, drawable, 0); - } - - private boolean isPasswordVisible() { - return ((mPasswordInput.getInputType() & InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) == - InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); - } - - private void hidePasswordButton() { - mPasswordInput.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); - } - - private void showPassword() { - mPasswordInput.setInputType( - InputType.TYPE_CLASS_TEXT | - InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | - InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS - ); - showViewPasswordButton(); - } - - private void hidePassword() { - mPasswordInput.setInputType( - InputType.TYPE_CLASS_TEXT | - InputType.TYPE_TEXT_VARIATION_PASSWORD | - InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS - ); - showViewPasswordButton(); - } - - /** - * Checks the credentials of the user in the root of the ownCloud server - * before creating a new local account. - * - * For basic authorization, a check of existence of the root folder is - * performed. - * - * For OAuth, starts the flow to get an access token; the credentials test - * is postponed until it is available. - * - * IMPORTANT ENTRY POINT 4 - */ - public void onOkClick() { - // this check should be unnecessary - if (mServerInfo.mVersion == null || - !mServerInfo.mVersion.isVersionValid() || - mServerInfo.mBaseUrl == null || - mServerInfo.mBaseUrl.length() == 0) { - mServerStatusIcon = R.drawable.ic_alert; - mServerStatusText = getResources().getString(R.string.auth_wtf_reenter_URL); - showServerStatus(); - mOkButton.setEnabled(false); - return; - } - - if (AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType(this)).equals(mAuthTokenType)) { - startOauthorization(); - } else if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType(this)) - .equals(mAuthTokenType)) { - startSamlBasedFederatedSingleSignOnAuthorization(); - } else { - checkBasicAuthorization(null, null); - } - } - - - /** - * Tests the credentials entered by the user performing a check of existence on - * the root folder of the ownCloud server. - */ - private void checkBasicAuthorization(@Nullable String webViewUsername, @Nullable String webViewPassword) { - /// get basic credentials entered by user - String username; - String password; - if (!webViewLoginMethod) { - username = mUsernameInput.getText().toString().trim(); - password = mPasswordInput.getText().toString(); - } else { - username = webViewUsername; - password = webViewPassword; - } - - /// be gentle with the user - IndeterminateProgressDialog dialog = - IndeterminateProgressDialog.newInstance(R.string.auth_trying_to_login, true); - dialog.show(getSupportFragmentManager(), WAIT_DIALOG_TAG); - - /// validate credentials accessing the root folder - OwnCloudCredentials credentials = OwnCloudCredentialsFactory.newBasicCredentials(username, password); - accessRootFolder(credentials); - } - - private void accessRootFolder(OwnCloudCredentials credentials) { - mAsyncTask = new AuthenticatorAsyncTask(this); - Object[] params = {mServerInfo.mBaseUrl, credentials}; - mAsyncTask.execute(params); - } - - - /** - * Starts the OAuth 'grant type' flow to get an access token, with - * a GET AUTHORIZATION request to the BUILT-IN authorization server. - */ - private void startOauthorization() { - // be gentle with the user - mAuthStatusIcon = R.drawable.progress_small; - mAuthStatusText = getResources().getString(R.string.oauth_login_connection); - showAuthStatus(); - - // GET AUTHORIZATION request - Uri uri = Uri.parse(mOAuthAuthEndpointText.getText().toString().trim()); - Uri.Builder uriBuilder = uri.buildUpon(); - uriBuilder.appendQueryParameter( - OAuth2Constants.KEY_RESPONSE_TYPE, getString(R.string.oauth2_response_type) - ); - uriBuilder.appendQueryParameter( - OAuth2Constants.KEY_REDIRECT_URI, getString(R.string.oauth2_redirect_uri) - ); - uriBuilder.appendQueryParameter( - OAuth2Constants.KEY_CLIENT_ID, getString(R.string.oauth2_client_id) - ); - uriBuilder.appendQueryParameter( - OAuth2Constants.KEY_SCOPE, getString(R.string.oauth2_scope) - ); - uri = uriBuilder.build(); - Log_OC.d(TAG, "Starting browser to view " + uri.toString()); - Intent i = new Intent(Intent.ACTION_VIEW, uri); - startActivity(i); - } - - - /** - * Starts the Web Single Sign On flow to get access to the root folder - * in the server. - */ - private void startSamlBasedFederatedSingleSignOnAuthorization() { - /// be gentle with the user - mAuthStatusIcon = R.drawable.progress_small; - mAuthStatusText = getResources().getString(R.string.auth_connecting_auth_server); - showAuthStatus(); - - /// Show SAML-based SSO web dialog - String targetUrl = mServerInfo.mBaseUrl - + AuthenticatorUrlUtils.getWebdavPath(mServerInfo.mVersion, mAuthTokenType, this); - SamlWebViewDialog dialog = SamlWebViewDialog.newInstance(targetUrl, targetUrl); - dialog.show(getSupportFragmentManager(), SAML_DIALOG_TAG); - } - - /** - * Callback method invoked when a RemoteOperation executed by this Activity finishes. - * - * Dispatches the operation flow to the right method. - */ - @Override - public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) { - - if (operation instanceof GetServerInfoOperation) { - if (operation.hashCode() == mWaitingForOpId) { - onGetServerInfoFinish(result); - } // else nothing ; only the last check operation is considered; - // multiple can be started if the user amends a URL quickly - - } else if (operation instanceof OAuth2GetAccessToken) { - onGetOAuthAccessTokenFinish(result); - - } else if (operation instanceof GetRemoteUserInfoOperation) { - onGetUserNameFinish(result); - } - - } - - private void onGetUserNameFinish(RemoteOperationResult result) { - mWaitingForOpId = Long.MAX_VALUE; - if (result.isSuccess()) { - boolean success = false; - String username; - if (result.getData().get(0) instanceof UserInfo) { - username = ((UserInfo) result.getData().get(0)).getDisplayName(); - } else { - username = (String) result.getData().get(0); - } - - if (mAction == ACTION_CREATE) { - if (!webViewLoginMethod) { - mUsernameInput.setText(username); - } - success = createAccount(result); - } else { - - if (!webViewLoginMethod && !mUsernameInput.getText().toString().trim().equals(username)) { - // fail - not a new account, but an existing one; disallow - result = new RemoteOperationResult(ResultCode.ACCOUNT_NOT_THE_SAME); - mAuthToken = ""; - updateAuthStatusIconAndText(result); - showAuthStatus(); - Log_OC.d(TAG, result.getLogMessage()); - } else { - try { - updateAccountAuthentication(); - success = true; - - } catch (AccountNotFoundException e) { - Log_OC.e(TAG, "Account " + mAccount + " was removed!", e); - DisplayUtils.showSnackMessage(findViewById(R.id.scroll), R.string.auth_account_does_not_exist); - finish(); - } - } - } - - if (success) { - finish(); - } - } else { - if (!webViewLoginMethod) { - int statusText = result.getCode() == ResultCode.MAINTENANCE_MODE ? R.string.maintenance_mode : R.string.auth_fail_get_user_name; - updateStatusIconFailUserName(statusText); - showAuthStatus(); - } - Log_OC.e(TAG, "Access to user name failed: " + result.getLogMessage()); - } - - } - - /** - * Processes the result of the server check performed when the user finishes the enter of the - * server URL. - * - * @param result Result of the check. - */ - private void onGetServerInfoFinish(RemoteOperationResult result) { - /// update activity state - mServerIsChecked = true; - mWaitingForOpId = Long.MAX_VALUE; - - // update server status, but don't show it yet - if (!webViewLoginMethod) { - updateServerStatusIconAndText(result); - } - - if (result.isSuccess()) { - /// SUCCESS means: - // 1. connection succeeded, and we know if it's SSL or not - // 2. server is installed - // 3. we got the server version - // 4. we got the authentication method required by the server - mServerInfo = (GetServerInfoOperation.ServerInfo) (result.getData().get(0)); - - // show outdated warning - if (getResources().getBoolean(R.bool.show_outdated_server_warning) && - mServerInfo.mVersion.compareTo(MainApp.OUTDATED_SERVER_VERSION) < 0) { - DisplayUtils.showServerOutdatedSnackbar(this); - } - - webViewLoginMethod = mServerInfo.mVersion.isWebLoginSupported() && !forceOldLoginMethod; - - if (webViewUser != null && !webViewUser.isEmpty() && - webViewPassword != null && !webViewPassword.isEmpty()) { - checkBasicAuthorization(webViewUser, webViewPassword); - } else if (webViewLoginMethod) { - // hide old login - setOldLoginVisibility(View.GONE); - - setContentView(R.layout.account_setup_webview); - mLoginWebView = findViewById(R.id.login_webview); - initWebViewLogin(mServerInfo.mBaseUrl); - } else { - // show old login - setOldLoginVisibility(View.VISIBLE); - } - - if (!authSupported(mServerInfo.mAuthMethod)) { - - if (!webViewLoginMethod) { - // overrides updateServerStatusIconAndText() - updateServerStatusIconNoRegularAuth(); - } - mServerIsValid = false; - - } else { - mServerIsValid = true; - } - - } else { - mServerIsValid = false; - } - - // refresh UI - if (!webViewLoginMethod) { - showRefreshButton(!mServerIsValid); - showServerStatus(); - mOkButton.setEnabled(mServerIsValid); - } - - if (!mServerIsValid) { - // hide old login - setOldLoginVisibility(View.GONE); - } - - /// very special case (TODO: move to a common place for all the remote operations) - if (result.getCode() == ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED) { - showUntrustedCertDialog(result); - } - } - - private void setOldLoginVisibility(int visible) { - mOkButton.setVisibility(visible); - mUsernameInputLayout.setVisibility(visible); - mPasswordInputLayout.setVisibility(visible); - } - - private boolean authSupported(AuthenticationMethod authMethod) { - return ((basicTokenType.equals(mAuthTokenType) && - AuthenticationMethod.BASIC_HTTP_AUTH.equals(authMethod)) || - (oauthTokenType.equals(mAuthTokenType) && - AuthenticationMethod.BEARER_TOKEN.equals(authMethod)) || - (samlTokenType.equals(mAuthTokenType) && - AuthenticationMethod.SAML_WEB_SSO.equals(authMethod)) - ); - } - - /** - * Chooses the right icon and text to show to the user for the received operation result. - * - * @param result Result of a remote operation performed in this activity - */ - private void updateServerStatusIconAndText(RemoteOperationResult result) { - mServerStatusIcon = R.drawable.ic_alert; // the most common case in the switch below - - switch (result.getCode()) { - case OK_SSL: - mServerStatusIcon = R.drawable.ic_lock_white; - mServerStatusText = getResources().getString(R.string.auth_secure_connection); - break; - - case OK_NO_SSL: - case OK: - if (mHostUrlInput.getText().toString().trim().toLowerCase(Locale.ROOT).startsWith(HTTP_PROTOCOL)) { - mServerStatusText = getResources().getString(R.string.auth_connection_established); - mServerStatusIcon = R.drawable.ic_ok; - } else { - mServerStatusText = getResources().getString(R.string.auth_nossl_plain_ok_title); - mServerStatusIcon = R.drawable.ic_lock_open_white; - } - break; - - case NO_NETWORK_CONNECTION: - mServerStatusIcon = R.drawable.no_network; - mServerStatusText = getResources().getString(R.string.auth_no_net_conn_title); - break; - - case SSL_RECOVERABLE_PEER_UNVERIFIED: - mServerStatusText = getResources().getString(R.string.auth_ssl_unverified_server_title); - break; - case BAD_OC_VERSION: - mServerStatusText = getResources().getString(R.string.auth_bad_oc_version_title); - break; - case WRONG_CONNECTION: - mServerStatusText = getResources().getString(R.string.auth_wrong_connection_title); - break; - case TIMEOUT: - mServerStatusText = getResources().getString(R.string.auth_timeout_title); - break; - case INCORRECT_ADDRESS: - mServerStatusText = getResources().getString(R.string.auth_incorrect_address_title); - break; - case SSL_ERROR: - mServerStatusText = getResources().getString(R.string.auth_ssl_general_error_title); - break; - case UNAUTHORIZED: - mServerStatusText = getResources().getString(R.string.auth_unauthorized); - break; - case HOST_NOT_AVAILABLE: - mServerStatusText = getResources().getString(R.string.auth_unknown_host_title); - break; - case INSTANCE_NOT_CONFIGURED: - mServerStatusText = getResources().getString(R.string.auth_not_configured_title); - break; - case FILE_NOT_FOUND: - mServerStatusText = getResources().getString(R.string.auth_incorrect_path_title); - break; - case OAUTH2_ERROR: - mServerStatusText = getResources().getString(R.string.auth_oauth_error); - break; - case OAUTH2_ERROR_ACCESS_DENIED: - mServerStatusText = getResources().getString(R.string.auth_oauth_error_access_denied); - break; - case UNHANDLED_HTTP_CODE: - mServerStatusText = getResources().getString(R.string.auth_unknown_error_http_title); - break; - case UNKNOWN_ERROR: - if (result.getException() != null && - !TextUtils.isEmpty(result.getException().getMessage())) { - mServerStatusText = getResources().getString( - R.string.auth_unknown_error_exception_title, - result.getException().getMessage() - ); - } else { - mServerStatusText = getResources().getString(R.string.auth_unknown_error_title); - } - break; - case OK_REDIRECT_TO_NON_SECURE_CONNECTION: - mServerStatusIcon = R.drawable.ic_lock_open_white; - mServerStatusText = getResources().getString(R.string.auth_redirect_non_secure_connection_title); - break; - case MAINTENANCE_MODE: - mServerStatusText = getResources().getString(R.string.maintenance_mode); - break; - case UNTRUSTED_DOMAIN: - mServerStatusText = getResources().getString(R.string.untrusted_domain); - break; - default: - mServerStatusText = ""; - mServerStatusIcon = 0; - break; - } - } - - - /** - * Chooses the right icon and text to show to the user for the received operation result. - * - * @param result Result of a remote operation performed in this activity - */ - private void updateAuthStatusIconAndText(RemoteOperationResult result) { - mAuthStatusIcon = R.drawable.ic_alert; // the most common case in the switch below - - switch (result.getCode()) { - case OK_SSL: - mAuthStatusIcon = R.drawable.ic_lock_white; - mAuthStatusText = getResources().getString(R.string.auth_secure_connection); - break; - - case OK_NO_SSL: - case OK: - if (mHostUrlInput.getText().toString().trim().toLowerCase(Locale.ROOT).startsWith(HTTP_PROTOCOL)) { - mAuthStatusText = getResources().getString(R.string.auth_connection_established); - mAuthStatusIcon = R.drawable.ic_ok; - } else { - mAuthStatusText = getResources().getString(R.string.auth_nossl_plain_ok_title); - mAuthStatusIcon = R.drawable.ic_lock_open_white; - } - break; - - case NO_NETWORK_CONNECTION: - mAuthStatusIcon = R.drawable.no_network; - mAuthStatusText = getResources().getString(R.string.auth_no_net_conn_title); - break; - - case SSL_RECOVERABLE_PEER_UNVERIFIED: - mAuthStatusText = getResources().getString(R.string.auth_ssl_unverified_server_title); - break; - case TIMEOUT: - mAuthStatusText = getResources().getString(R.string.auth_timeout_title); - break; - case HOST_NOT_AVAILABLE: - mAuthStatusText = getResources().getString(R.string.auth_unknown_host_title); - break; - case UNHANDLED_HTTP_CODE: - default: - mAuthStatusText = ErrorMessageAdapter.getErrorCauseMessage(result, null, getResources()); - } - } - - private void updateStatusIconFailUserName(int failedStatusText) { - mAuthStatusIcon = R.drawable.ic_alert; - mAuthStatusText = getResources().getString(failedStatusText); - } - - private void updateServerStatusIconNoRegularAuth() { - mServerStatusIcon = R.drawable.ic_alert; - mServerStatusText = getResources().getString(R.string.auth_can_not_auth_against_server); - } - - /** - * Processes the result of the request for and access token send - * to an OAuth authorization server. - * - * @param result Result of the operation. - */ - private void onGetOAuthAccessTokenFinish(RemoteOperationResult result) { - mWaitingForOpId = Long.MAX_VALUE; - dismissDialog(WAIT_DIALOG_TAG); - - if (result.isSuccess()) { - /// be gentle with the user - IndeterminateProgressDialog dialog = - IndeterminateProgressDialog.newInstance(R.string.auth_trying_to_login, true); - dialog.show(getSupportFragmentManager(), WAIT_DIALOG_TAG); - - /// time to test the retrieved access token on the ownCloud server - @SuppressWarnings("unchecked") - Map tokens = (Map) (result.getData().get(0)); - mAuthToken = tokens.get(OAuth2Constants.KEY_ACCESS_TOKEN); - Log_OC.d(TAG, "Got ACCESS TOKEN: " + mAuthToken); - - /// validate token accessing to root folder / getting session - OwnCloudCredentials credentials = OwnCloudCredentialsFactory.newBearerCredentials( - mAuthToken); - accessRootFolder(credentials); - - } else { - updateAuthStatusIconAndText(result); - showAuthStatus(); - Log_OC.d(TAG, "Access failed: " + result.getLogMessage()); - } - } - - - /** - * Processes the result of the access check performed to try the user credentials. - * - * Creates a new account through the AccountManager. - * - * @param result Result of the operation. - */ - @Override - public void onAuthenticatorTaskCallback(RemoteOperationResult result) { - mWaitingForOpId = Long.MAX_VALUE; - dismissDialog(WAIT_DIALOG_TAG); - mAsyncTask = null; - - if (result.isSuccess()) { - Log_OC.d(TAG, "Successful access - time to save the account"); - - boolean success = false; - - if (mAction == ACTION_CREATE) { - success = createAccount(result); - - } else { - try { - updateAccountAuthentication(); - success = true; - - } catch (AccountNotFoundException e) { - Log_OC.e(TAG, "Account " + mAccount + " was removed!", e); - DisplayUtils.showSnackMessage(findViewById(R.id.scroll), R.string.auth_account_does_not_exist); - finish(); - } - } - - // Reset webView - webViewPassword = null; - webViewUser = null; - forceOldLoginMethod = false; - deleteCookies(); - - if (success) { - finish(); - } else { - // init webView again - if (mLoginWebView != null) { - mLoginWebView.setVisibility(View.GONE); - } - setContentView(R.layout.account_setup); - - initOverallUi(); - - CustomEditText serverAddressField = findViewById(R.id.hostUrlInput); - serverAddressField.setText(mServerInfo.mBaseUrl); - - findViewById(R.id.oauth_onOff_check).setVisibility(View.GONE); - findViewById(R.id.server_status_text).setVisibility(View.GONE); - mAuthStatusView = findViewById(R.id.auth_status_text); - - showAuthStatus(); - } - - } else if (result.isServerFail() || result.isException()) { - /// server errors or exceptions in authorization take to requiring a new check of - /// the server - mServerIsChecked = true; - mServerIsValid = false; - mServerInfo = new GetServerInfoOperation.ServerInfo(); - - // update status icon and text - updateServerStatusIconAndText(result); - showServerStatus(); - mAuthStatusIcon = 0; - mAuthStatusText = ""; - if (!webViewLoginMethod) { - showAuthStatus(); - - // update input controls state - showRefreshButton(true); - mOkButton.setEnabled(false); - } - - // very special case (TODO: move to a common place for all the remote operations) - if (result.getCode() == ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED) { - showUntrustedCertDialog(result); - } - - } else { // authorization fail due to client side - probably wrong credentials - if (webViewLoginMethod) { - mLoginWebView = findViewById(R.id.login_webview); - initWebViewLogin(mServerInfo.mBaseUrl); - - DisplayUtils.showSnackMessage(this, mLoginWebView, R.string.auth_access_failed, result.getLogMessage()); - } else { - updateAuthStatusIconAndText(result); - showAuthStatus(); - } - // reset webview - webViewPassword = null; - webViewUser = null; - deleteCookies(); - - Log_OC.d(TAG, "Access failed: " + result.getLogMessage()); - } - } - - - /** - * Updates the authentication token. - * - * Sets the proper response so that the AccountAuthenticator that started this activity - * saves a new authorization token for mAccount. - * - * Kills the session kept by OwnCloudClientManager so that a new one will created with - * the new credentials when needed. - */ - private void updateAccountAuthentication() throws AccountNotFoundException { - String accountType = MainApp.getAccountType(this); - - Bundle response = new Bundle(); - response.putString(AccountManager.KEY_ACCOUNT_NAME, mAccount.name); - response.putString(AccountManager.KEY_ACCOUNT_TYPE, mAccount.type); - - if (AccountTypeUtils.getAuthTokenTypeAccessToken(accountType).equals(mAuthTokenType)) { - response.putString(AccountManager.KEY_AUTHTOKEN, mAuthToken); - // the next line is necessary, notifications are calling directly to the - // AuthenticatorActivity to update, without AccountManager intervention - mAccountMgr.setAuthToken(mAccount, mAuthTokenType, mAuthToken); - - } else if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(accountType).equals(mAuthTokenType)) { - response.putString(AccountManager.KEY_AUTHTOKEN, mAuthToken); - // the next line is necessary; by now, notifications are calling directly to the - // AuthenticatorActivity to update, without AccountManager intervention - mAccountMgr.setAuthToken(mAccount, mAuthTokenType, mAuthToken); - - } else { - if (!webViewLoginMethod) { - response.putString(AccountManager.KEY_AUTHTOKEN, mPasswordInput.getText().toString()); - mAccountMgr.setPassword(mAccount, mPasswordInput.getText().toString()); - } else { - response.putString(AccountManager.KEY_AUTHTOKEN, webViewPassword); - mAccountMgr.setPassword(mAccount, webViewPassword); - } - } - - // remove managed clients for this account to enforce creation with fresh credentials - OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, this); - OwnCloudClientManagerFactory.getDefaultSingleton().removeClientFor(ocAccount); - - setAccountAuthenticatorResult(response); - final Intent intent = new Intent(); - intent.putExtras(response); - setResult(RESULT_OK, intent); - - } - - - /** - * Creates a new account through the Account Authenticator that started this activity. - * - * This makes the account permanent. - * - * TODO Decide how to name the OAuth accounts - */ - @SuppressFBWarnings("DMI") - private boolean createAccount(RemoteOperationResult authResult) { - String accountType = MainApp.getAccountType(this); - - // create and save new ownCloud account - boolean isOAuth = AccountTypeUtils.getAuthTokenTypeAccessToken(accountType).equals(mAuthTokenType); - boolean isSaml = AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(accountType).equals(mAuthTokenType); - - String lastPermanentLocation = authResult.getLastPermanentLocation(); - if (lastPermanentLocation != null) { - mServerInfo.mBaseUrl = AuthenticatorUrlUtils.trimWebdavSuffix(lastPermanentLocation); - } - - Uri uri = Uri.parse(mServerInfo.mBaseUrl); - String username; - if (!webViewLoginMethod) { - username = mUsernameInput.getText().toString().trim(); - } else { - username = webViewUser; - } - if (isOAuth) { - username = "OAuth_user" + (new java.util.Random(System.currentTimeMillis())).nextLong(); - } - - String accountName = com.owncloud.android.lib.common.accounts.AccountUtils.buildAccountName(uri, username); - Account newAccount = new Account(accountName, accountType); - if (AccountUtils.exists(newAccount, getApplicationContext())) { - // fail - not a new account, but an existing one; disallow - RemoteOperationResult result = new RemoteOperationResult(ResultCode.ACCOUNT_NOT_NEW); - - updateAuthStatusIconAndText(result); - showAuthStatus(); - - Log_OC.d(TAG, result.getLogMessage()); - return false; - - } else { - mAccount = newAccount; - - if (isOAuth || isSaml) { - // with external authorizations, the password is never input in the app - mAccountMgr.addAccountExplicitly(mAccount, "", null); - } else { - if (!webViewLoginMethod) { - mAccountMgr.addAccountExplicitly(mAccount, mPasswordInput.getText().toString(), null); - } else { - mAccountMgr.addAccountExplicitly(mAccount, webViewPassword, null); - } - } - - // include account version with the new account - mAccountMgr.setUserData(mAccount, Constants.KEY_OC_ACCOUNT_VERSION, - Integer.toString(AccountUtils.ACCOUNT_VERSION)); - - /// add the new account as default in preferences, if there is none already - Account defaultAccount = AccountUtils.getCurrentOwnCloudAccount(this); - if (defaultAccount == null) { - SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); - editor.putString("select_oc_account", accountName); - editor.apply(); - } - - /// prepare result to return to the Authenticator - // TODO check again what the Authenticator makes with it; probably has the same - // effect as addAccountExplicitly, but it's not well done - final Intent intent = new Intent(); - intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, accountType); - intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, mAccount.name); - intent.putExtra(AccountManager.KEY_USERDATA, username); - if (isOAuth || isSaml) { - mAccountMgr.setAuthToken(mAccount, mAuthTokenType, mAuthToken); - } - /// add user data to the new account; TODO probably can be done in the last parameter - // addAccountExplicitly, or in KEY_USERDATA - mAccountMgr.setUserData(mAccount, Constants.KEY_OC_VERSION, mServerInfo.mVersion.getVersion()); - mAccountMgr.setUserData(mAccount, Constants.KEY_OC_BASE_URL, mServerInfo.mBaseUrl); - - if (authResult.getData() != null) { - try { - UserInfo userInfo = (UserInfo) authResult.getData().get(0); - mAccountMgr.setUserData(mAccount, Constants.KEY_DISPLAY_NAME, userInfo.getDisplayName()); - mAccountMgr.setUserData(mAccount, Constants.KEY_USER_ID, userInfo.getId()); - } catch (ClassCastException c) { - Log_OC.w(TAG, "Couldn't get display name for " + username); - } - } else { - Log_OC.w(TAG, "Couldn't get display name for " + username); - } - - if (isSaml) { - mAccountMgr.setUserData(mAccount, Constants.KEY_SUPPORTS_SAML_WEB_SSO, "TRUE"); - } else if (isOAuth) { - mAccountMgr.setUserData(mAccount, Constants.KEY_SUPPORTS_OAUTH2, "TRUE"); - } - - setAccountAuthenticatorResult(intent.getExtras()); - setResult(RESULT_OK, intent); - - return true; - } - } - - - /** - * Starts and activity to open the 'new account' page in the ownCloud web site - * - * @param view 'Account register' button - */ - public void onRegisterClick(View view) { - Intent register = new Intent( - Intent.ACTION_VIEW, Uri.parse(getString(R.string.welcome_link_url)) - ); - setResult(RESULT_CANCELED); - startActivity(register); - } - - - /** - * Updates the content and visibility state of the icon and text associated - * to the last check on the ownCloud server. - */ - private void showServerStatus() { - if (mServerStatusIcon == 0 && "".equals(mServerStatusText)) { - mServerStatusView.setVisibility(View.INVISIBLE); - } else { - mServerStatusView.setText(mServerStatusText); - mServerStatusView.setCompoundDrawablesWithIntrinsicBounds(mServerStatusIcon, 0, 0, 0); - mServerStatusView.setVisibility(View.VISIBLE); - } - } - - - /** - * Updates the content and visibility state of the icon and text associated - * to the interactions with the OAuth authorization server. - */ - private void showAuthStatus() { - if (mAuthStatusIcon == 0 && "".equals(mAuthStatusText)) { - mAuthStatusView.setVisibility(View.INVISIBLE); - } else { - mAuthStatusView.setText(mAuthStatusText); - mAuthStatusView.setCompoundDrawablesWithIntrinsicBounds(mAuthStatusIcon, 0, 0, 0); - mAuthStatusView.setVisibility(View.VISIBLE); - } - } - - - private void showRefreshButton(boolean show) { - if (webViewLoginMethod && mRefreshButton != null) { - if (show) { - mRefreshButton.setVisibility(View.VISIBLE); - } else { - mRefreshButton.setVisibility(View.GONE); - } - } - } - - /** - * Called when the eye icon in the password field is clicked. - * - * Toggles the visibility of the password in the field. - */ - public void onViewPasswordClick() { - int selectionStart = mPasswordInput.getSelectionStart(); - int selectionEnd = mPasswordInput.getSelectionEnd(); - if (isPasswordVisible()) { - hidePassword(); - } else { - showPassword(); - } - mPasswordInput.setSelection(selectionStart, selectionEnd); - } - - - /** - * Called when the checkbox for OAuth authorization is clicked. - * - * Hides or shows the input fields for user & password. - * - * @param view 'View password' 'button' - */ - public void onCheckClick(View view) { - CheckBox oAuth2Check = (CheckBox) view; - if (oAuth2Check.isChecked()) { - mAuthTokenType = oauthTokenType; - } else { - mAuthTokenType = basicTokenType; - } - updateAuthenticationPreFragmentVisibility(); - } - - - /** - * Called when the 'action' button in an IME is pressed ('enter' in software keyboard). - * - * Used to trigger the authentication check when the user presses 'enter' after writing the - * password, or to throw the server test when the only field on screen is the URL input field. - */ - @Override - public boolean onEditorAction(TextView inputField, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_DONE && inputField != null && - inputField.equals(mPasswordInput)) { - if (mOkButton.isEnabled()) { - mOkButton.performClick(); - } - - } else if (actionId == EditorInfo.IME_ACTION_NEXT && inputField != null && - inputField.equals(mHostUrlInput)) { - checkOcServer(); - } - return false; // always return false to grant that the software keyboard is hidden anyway - } - - - private abstract static class RightDrawableOnTouchListener implements OnTouchListener { - - private int fuzz = 75; - - /** - * {@inheritDoc} - */ - @Override - public boolean onTouch(View view, MotionEvent event) { - Drawable rightDrawable = null; - if (view instanceof TextView) { - Drawable[] drawables = ((TextView) view).getCompoundDrawables(); - if (drawables.length > 2) { - rightDrawable = drawables[2]; - } - } - if (rightDrawable != null) { - final int x = (int) event.getX(); - final int y = (int) event.getY(); - final Rect bounds = rightDrawable.getBounds(); - if (x >= (view.getRight() - bounds.width() - fuzz) && - x <= (view.getRight() - view.getPaddingRight() + fuzz) && - y >= (view.getPaddingTop() - fuzz) && - y <= (view.getHeight() - view.getPaddingBottom()) + fuzz) { - - return onDrawableTouch(event); - } - } - return false; - } - - public abstract boolean onDrawableTouch(final MotionEvent event); - } - - - private void getRemoteUserNameOperation(String sessionCookie) { - - Intent getUserNameIntent = new Intent(); - getUserNameIntent.setAction(OperationsService.ACTION_GET_USER_NAME); - getUserNameIntent.putExtra(OperationsService.EXTRA_SERVER_URL, mServerInfo.mBaseUrl); - getUserNameIntent.putExtra(OperationsService.EXTRA_COOKIE, sessionCookie); - - if (mOperationsServiceBinder != null) { - mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getUserNameIntent); - } - } - - - @Override - public void onSsoFinished(String sessionCookie) { - if (sessionCookie != null && sessionCookie.length() > 0) { - Log_OC.d(TAG, "Successful SSO - time to save the account"); - mAuthToken = sessionCookie; - getRemoteUserNameOperation(sessionCookie); - Fragment fd = getSupportFragmentManager().findFragmentByTag(SAML_DIALOG_TAG); - if (fd instanceof DialogFragment) { - Dialog d = ((DialogFragment) fd).getDialog(); - if (d != null && d.isShowing()) { - d.dismiss(); - } - } - - } else { - // TODO - show fail - Log_OC.d(TAG, "SSO failed"); - } - - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType(this)).equals(mAuthTokenType) && - mHostUrlInput.hasFocus() && event.getAction() == MotionEvent.ACTION_DOWN) { - checkOcServer(); - } - return super.onTouchEvent(event); - } - - - /** - * Show untrusted cert dialog - */ - public void showUntrustedCertDialog(X509Certificate x509Certificate, SslError error, SslErrorHandler handler) { - // Show a dialog with the certificate info - SslUntrustedCertDialog dialog; - if (x509Certificate == null) { - dialog = SslUntrustedCertDialog.newInstanceForEmptySslError(error, handler); - } else { - dialog = SslUntrustedCertDialog.newInstanceForFullSslError(x509Certificate, error, handler); - } - FragmentManager fm = getSupportFragmentManager(); - FragmentTransaction ft = fm.beginTransaction(); - ft.addToBackStack(null); - dialog.show(ft, UNTRUSTED_CERT_DIALOG_TAG); - } - - - /** - * Show untrusted cert dialog - */ - private void showUntrustedCertDialog(RemoteOperationResult result) { - // Show a dialog with the certificate info - SslUntrustedCertDialog dialog = SslUntrustedCertDialog. - newInstanceForFullSslError((CertificateCombinedException) result.getException()); - FragmentManager fm = getSupportFragmentManager(); - FragmentTransaction ft = fm.beginTransaction(); - ft.addToBackStack(null); - dialog.show(ft, UNTRUSTED_CERT_DIALOG_TAG); - - } - - /** - * Called from SslValidatorDialog when a new server certificate was correctly saved. - */ - public void onSavedCertificate() { - Fragment fd = getSupportFragmentManager().findFragmentByTag(SAML_DIALOG_TAG); - if (fd == null) { - // if SAML dialog is not shown, - // the SslDialog was shown due to an SSL error in the server check - checkOcServer(); - } - } - - /** - * Called from SslValidatorDialog when a new server certificate could not be saved - * when the user requested it. - */ - @Override - public void onFailedSavingCertificate() { - dismissDialog(SAML_DIALOG_TAG); - DisplayUtils.showSnackMessage(this, R.string.ssl_validator_not_saved); - } - - @Override - public void onCancelCertificate() { - dismissDialog(SAML_DIALOG_TAG); - } - - - private void doOnResumeAndBound() { - //Log_OC.e(TAG, "registering to listen for operation callbacks" ); - mOperationsServiceBinder.addOperationListener(AuthenticatorActivity.this, mHandler); - if (mWaitingForOpId <= Integer.MAX_VALUE) { - mOperationsServiceBinder.dispatchResultIfFinished((int) mWaitingForOpId, this); - } - - if (!webViewLoginMethod && mHostUrlInput.getText() != null && mHostUrlInput.getText().length() > 0 - && !mServerIsChecked) { - checkOcServer(); - } - } - - - private void dismissDialog(String dialogTag) { - Fragment frag = getSupportFragmentManager().findFragmentByTag(dialogTag); - if (frag instanceof DialogFragment) { - DialogFragment dialog = (DialogFragment) frag; - - try { - dialog.dismiss(); - } catch (IllegalStateException e) { - Log_OC.e(TAG, e.getMessage()); - dialog.dismissAllowingStateLoss(); - } - } - } - - - /** - * Implements callback methods for service binding. - */ - private class OperationsServiceConnection implements ServiceConnection { - - @Override - public void onServiceConnected(ComponentName component, IBinder service) { - if (component.equals( - new ComponentName(AuthenticatorActivity.this, OperationsService.class) - )) { - mOperationsServiceBinder = (OperationsServiceBinder) service; - - doOnResumeAndBound(); - - } - - } - - @Override - public void onServiceDisconnected(ComponentName component) { - if (component.equals( - new ComponentName(AuthenticatorActivity.this, OperationsService.class) - )) { - Log_OC.e(TAG, "Operations service crashed"); - mOperationsServiceBinder = null; - } - } - } - - /** - * Create and show dialog for request authentication to the user - * - * @param webView Web view to embed into the authentication dialog. - * @param handler Object responsible for catching and recovering HTTP authentication fails. - */ - public void createAuthenticationDialog(WebView webView, HttpAuthHandler handler) { - - // Show a dialog with the certificate info - CredentialsDialogFragment dialog = CredentialsDialogFragment.newInstanceForCredentials(webView, handler); - FragmentManager fm = getSupportFragmentManager(); - FragmentTransaction ft = fm.beginTransaction(); - ft.addToBackStack(null); - dialog.setCancelable(false); - dialog.show(ft, CREDENTIALS_DIALOG_TAG); - - if (!mIsFirstAuthAttempt) { - DisplayUtils.showSnackMessage(this, R.string.saml_authentication_wrong_pass); - } else { - mIsFirstAuthAttempt = false; - } - } - - /** - * For retrieving the clicking on authentication cancel button. - */ - public void doNegativeAuthenticationDialogClick() { - mIsFirstAuthAttempt = true; - } -} +/* + * ownCloud Android client application + * + * @author Bartek Przybylski + * @author David A. Velasco + * @author masensio + * @author Mario Danic + * Copyright (C) 2012 Bartek Przybylski + * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * All changes by Mario Danic are distributed under the following terms: + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program 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 program. If not, see . + * + */ + +package com.owncloud.android.authentication; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.net.http.SslError; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.support.annotation.Nullable; +import android.support.design.widget.Snackbar; +import android.support.design.widget.TextInputLayout; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.text.Editable; +import android.text.InputType; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.AndroidRuntimeException; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnFocusChangeListener; +import android.view.View.OnTouchListener; +import android.view.inputmethod.EditorInfo; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.webkit.HttpAuthHandler; +import android.webkit.SslErrorHandler; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; + +import com.owncloud.android.MainApp; +import com.owncloud.android.R; +import com.owncloud.android.authentication.SsoWebViewClient.SsoWebViewClientListener; +import com.owncloud.android.lib.common.OwnCloudAccount; +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; +import com.owncloud.android.lib.common.OwnCloudCredentials; +import com.owncloud.android.lib.common.OwnCloudCredentialsFactory; +import com.owncloud.android.lib.common.UserInfo; +import com.owncloud.android.lib.common.accounts.AccountTypeUtils; +import com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException; +import com.owncloud.android.lib.common.accounts.AccountUtils.Constants; +import com.owncloud.android.lib.common.network.CertificateCombinedException; +import com.owncloud.android.lib.common.network.NetworkUtils; +import com.owncloud.android.lib.common.operations.OnRemoteOperationListener; +import com.owncloud.android.lib.common.operations.RemoteOperation; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; +import com.owncloud.android.lib.common.utils.Log_OC; +import com.owncloud.android.lib.resources.status.OwnCloudVersion; +import com.owncloud.android.lib.resources.users.GetRemoteUserInfoOperation; +import com.owncloud.android.operations.DetectAuthenticationMethodOperation.AuthenticationMethod; +import com.owncloud.android.operations.GetServerInfoOperation; +import com.owncloud.android.operations.OAuth2GetAccessToken; +import com.owncloud.android.services.OperationsService; +import com.owncloud.android.services.OperationsService.OperationsServiceBinder; +import com.owncloud.android.ui.activity.FirstRunActivity; +import com.owncloud.android.ui.components.CustomEditText; +import com.owncloud.android.ui.dialog.CredentialsDialogFragment; +import com.owncloud.android.ui.dialog.IndeterminateProgressDialog; +import com.owncloud.android.ui.dialog.SamlWebViewDialog; +import com.owncloud.android.ui.dialog.SslUntrustedCertDialog; +import com.owncloud.android.ui.dialog.SslUntrustedCertDialog.OnSslUntrustedCertListener; +import com.owncloud.android.utils.DisplayUtils; +import com.owncloud.android.utils.ErrorMessageAdapter; + +import java.io.InputStream; +import java.net.URLDecoder; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * This Activity is used to add an ownCloud account to the App + */ +public class AuthenticatorActivity extends AccountAuthenticatorActivity + implements OnRemoteOperationListener, OnFocusChangeListener, OnEditorActionListener, + SsoWebViewClientListener, OnSslUntrustedCertListener, + AuthenticatorAsyncTask.OnAuthenticatorTaskListener { + + private static final String TAG = AuthenticatorActivity.class.getSimpleName(); + + public static final String EXTRA_ACTION = "ACTION"; + public static final String EXTRA_ACCOUNT = "ACCOUNT"; + public static final String EXTRA_USE_PROVIDER_AS_WEBLOGIN = "USE_PROVIDER_AS_WEBLOGIN"; + + private static final String KEY_AUTH_TOKEN_TYPE = "AUTH_TOKEN_TYPE"; + + private static final String KEY_HOST_URL_TEXT = "HOST_URL_TEXT"; + private static final String KEY_OC_VERSION = "OC_VERSION"; + private static final String KEY_SERVER_VALID = "SERVER_VALID"; + private static final String KEY_SERVER_CHECKED = "SERVER_CHECKED"; + private static final String KEY_SERVER_STATUS_TEXT = "SERVER_STATUS_TEXT"; + private static final String KEY_SERVER_STATUS_ICON = "SERVER_STATUS_ICON"; + private static final String KEY_IS_SSL_CONN = "IS_SSL_CONN"; + private static final String KEY_PASSWORD_EXPOSED = "PASSWORD_VISIBLE"; + private static final String KEY_AUTH_STATUS_TEXT = "AUTH_STATUS_TEXT"; + private static final String KEY_AUTH_STATUS_ICON = "AUTH_STATUS_ICON"; + private static final String KEY_SERVER_AUTH_METHOD = "SERVER_AUTH_METHOD"; + private static final String KEY_WAITING_FOR_OP_ID = "WAITING_FOR_OP_ID"; + private static final String KEY_AUTH_TOKEN = "AUTH_TOKEN"; + + private static final String AUTH_ON = "on"; + private static final String AUTH_OPTIONAL = "optional"; + + public static final byte ACTION_CREATE = 0; + public static final byte ACTION_UPDATE_EXPIRED_TOKEN = 2; // detected by the app + + private static final String UNTRUSTED_CERT_DIALOG_TAG = "UNTRUSTED_CERT_DIALOG"; + private static final String SAML_DIALOG_TAG = "SAML_DIALOG"; + private static final String WAIT_DIALOG_TAG = "WAIT_DIALOG"; + private static final String CREDENTIALS_DIALOG_TAG = "CREDENTIALS_DIALOG"; + private static final String KEY_AUTH_IS_FIRST_ATTEMPT_TAG = "KEY_AUTH_IS_FIRST_ATTEMPT"; + + private static final String KEY_USERNAME = "USERNAME"; + private static final String KEY_PASSWORD = "PASSWORD"; + private static final String KEY_ASYNC_TASK_IN_PROGRESS = "AUTH_IN_PROGRESS"; + private static final String WEB_LOGIN = "/index.php/login/flow"; + public static final String PROTOCOL_SUFFIX = "://"; + public static final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"; + public static final String HTTPS_PROTOCOL = "https://"; + public static final String HTTP_PROTOCOL = "http://"; + + public static final String REGULAR_SERVER_INPUT_TYPE = "regular"; + public static final String SUBDOMAIN_SERVER_INPUT_TYPE = "prefix"; + public static final String DIRECTORY_SERVER_INPUT_TYPE = "suffix"; + + /// parameters from EXTRAs in starter Intent + private byte mAction; + private Account mAccount; + private String mAuthTokenType; + + /// activity-level references / state + private final Handler mHandler = new Handler(); + private ServiceConnection mOperationsServiceConnection = null; + private OperationsServiceBinder mOperationsServiceBinder = null; + private AccountManager mAccountMgr; + private Uri mNewCapturedUriFromOAuth2Redirection; + + /// Server PRE-Fragment elements + private CustomEditText mHostUrlInput; + private View mRefreshButton; + private TextView mServerStatusView; + + private TextWatcher mHostUrlInputWatcher; + private String mServerStatusText = ""; + private int mServerStatusIcon; + + private boolean mServerIsChecked; + private boolean mServerIsValid; + + private GetServerInfoOperation.ServerInfo mServerInfo = new GetServerInfoOperation.ServerInfo(); + + /// Authentication PRE-Fragment elements + private CheckBox mOAuth2Check; + private TextView mOAuthAuthEndpointText; + private TextView mOAuthTokenEndpointText; + private EditText mUsernameInput; + private EditText mPasswordInput; + private View mOkButton; + private TextView mAuthStatusView; + private ImageButton mTestServerButton; + + private WebView mLoginWebView; + + private String mAuthStatusText = ""; + private int mAuthStatusIcon; + + private String mAuthToken = ""; + private AuthenticatorAsyncTask mAsyncTask; + + private boolean mIsFirstAuthAttempt; + + /// Identifier of operation in progress which result shouldn't be lost + private long mWaitingForOpId = Long.MAX_VALUE; + + private String basicTokenType; + private String oauthTokenType; + private String samlTokenType; + + private boolean webViewLoginMethod; + private String webViewUser; + private String webViewPassword; + private TextInputLayout mUsernameInputLayout; + private TextInputLayout mPasswordInputLayout; + private boolean forceOldLoginMethod; + + /** + * {@inheritDoc} + * + * IMPORTANT ENTRY POINT 1: activity is shown to the user + */ + @Override + protected void onCreate(Bundle savedInstanceState) { + //Log_OC.e(TAG, "onCreate init"); + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + FirstRunActivity.runIfNeeded(this); + } + + basicTokenType = AccountTypeUtils.getAuthTokenTypePass(MainApp.getAccountType(this)); + oauthTokenType = AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType(this)); + samlTokenType = AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType(this)); + + // delete cookies for webView + deleteCookies(); + + // Workaround, for fixing a problem with Android Library Support v7 19 + //getWindow().requestFeature(Window.FEATURE_NO_TITLE); + if (getSupportActionBar() != null) { + getSupportActionBar().hide(); + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + getSupportActionBar().setDisplayShowHomeEnabled(false); + getSupportActionBar().setDisplayShowTitleEnabled(false); + } + + mIsFirstAuthAttempt = true; + + /// init activity state + mAccountMgr = AccountManager.get(this); + mNewCapturedUriFromOAuth2Redirection = null; + + /// get input values + mAction = getIntent().getByteExtra(EXTRA_ACTION, ACTION_CREATE); + mAccount = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT); + if (savedInstanceState == null) { + initAuthTokenType(); + } else { + mAuthTokenType = savedInstanceState.getString(KEY_AUTH_TOKEN_TYPE); + mWaitingForOpId = savedInstanceState.getLong(KEY_WAITING_FOR_OP_ID); + mIsFirstAuthAttempt = savedInstanceState.getBoolean(KEY_AUTH_IS_FIRST_ATTEMPT_TAG); + } + + String webloginUrl; + boolean showLegacyLogin; + if (getIntent().getBooleanExtra(EXTRA_USE_PROVIDER_AS_WEBLOGIN, false)) { + webViewLoginMethod = true; + webloginUrl = getString(R.string.provider_registration_server); + showLegacyLogin = false; + } else { + webViewLoginMethod = !TextUtils.isEmpty(getResources().getString(R.string.webview_login_url)); + webloginUrl = null; + showLegacyLogin = true; + } + + if (webViewLoginMethod) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + + /// load user interface + if (!webViewLoginMethod) { + setContentView(R.layout.account_setup); + + /// initialize general UI elements + initOverallUi(); + + findViewById(R.id.centeredRefreshButton).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + checkOcServer(); + } + }); + + findViewById(R.id.embeddedRefreshButton).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + checkOcServer(); + } + }); + + /// initialize block to be moved to single Fragment to check server and get info about it + + /// initialize block to be moved to single Fragment to retrieve and validate credentials + initAuthorizationPreFragment(savedInstanceState); + + } else { + setContentView(R.layout.account_setup_webview); + mLoginWebView = findViewById(R.id.login_webview); + initWebViewLogin(webloginUrl, showLegacyLogin); + } + + initServerPreFragment(savedInstanceState); + } + + private void deleteCookies() { + try { + CookieSyncManager.createInstance(this); + CookieManager cookieManager = CookieManager.getInstance(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + cookieManager.removeAllCookies(null); + } else { + cookieManager.removeAllCookie(); + } + } catch (AndroidRuntimeException e) { + Log_OC.e(TAG, e.getMessage()); + } + } + + private static String getWebLoginUserAgent() { + return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) + + Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL; + } + + @SuppressLint("SetJavaScriptEnabled") + private void initWebViewLogin(String baseURL, boolean showLegacyLogin) { + mLoginWebView.setVisibility(View.GONE); + + final ProgressBar progressBar = findViewById(R.id.login_webview_progress_bar); + + mLoginWebView.getSettings().setAllowFileAccess(false); + mLoginWebView.getSettings().setJavaScriptEnabled(true); + mLoginWebView.getSettings().setDomStorageEnabled(true); + mLoginWebView.getSettings().setUserAgentString(getWebLoginUserAgent()); + mLoginWebView.getSettings().setSaveFormData(false); + mLoginWebView.getSettings().setSavePassword(false); + + Map headers = new HashMap<>(); + headers.put(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE); + + String url; + if (baseURL != null && !baseURL.isEmpty()) { + url = baseURL; + } else { + url = getResources().getString(R.string.webview_login_url); + } + + mLoginWebView.loadUrl(url, headers); + + setClient(progressBar); + + // show snackbar after 60s to switch back to old login method + if (showLegacyLogin) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + DisplayUtils.createSnackbar(mLoginWebView, R.string.fallback_weblogin_text, Snackbar.LENGTH_INDEFINITE) + .setActionTextColor(getResources().getColor(R.color.primary_dark)) + .setAction(R.string.fallback_weblogin_back, new View.OnClickListener() { + @Override + public void onClick(View v) { + mLoginWebView.setVisibility(View.INVISIBLE); + webViewLoginMethod = false; + + setContentView(R.layout.account_setup); + + // initialize general UI elements + initOverallUi(); + + mPasswordInputLayout.setVisibility(View.VISIBLE); + mUsernameInputLayout.setVisibility(View.VISIBLE); + mUsernameInput.requestFocus(); + mOAuth2Check.setVisibility(View.INVISIBLE); + mAuthStatusView.setVisibility(View.INVISIBLE); + mServerStatusView.setVisibility(View.INVISIBLE); + mTestServerButton.setVisibility(View.INVISIBLE); + forceOldLoginMethod = true; + mOkButton.setVisibility(View.VISIBLE); + + initServerPreFragment(null); + + mHostUrlInput.setText(baseURL); + + checkOcServer(); + } + }).show(); + } + }, 60 * 1000); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mLoginWebView != null && event.getAction() == KeyEvent.ACTION_DOWN) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + if (mLoginWebView.canGoBack()) { + mLoginWebView.goBack(); + } else { + finish(); + } + return true; + } + + } + return super.onKeyDown(keyCode, event); + } + + private void setClient(ProgressBar progressBar) { + mLoginWebView.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (url.startsWith(getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/")) { + parseAndLoginFromWebView(url); + return true; + } + return false; + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + + progressBar.setVisibility(View.GONE); + mLoginWebView.setVisibility(View.VISIBLE); + } + + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + X509Certificate cert = SsoWebViewClient.getX509CertificateFromError(error); + + try { + if (cert != null && NetworkUtils.isCertInKnownServersStore(cert, getApplicationContext())) { + handler.proceed(); + } else { + showUntrustedCertDialog(cert, error, handler); + } + } catch (Exception e) { + Log_OC.e(TAG, "Cert could not be verified"); + } + } + + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + progressBar.setVisibility(View.GONE); + mLoginWebView.setVisibility(View.VISIBLE); + + InputStream resources = getResources().openRawResource(R.raw.custom_error); + String customError = DisplayUtils.getData(resources); + + if (!customError.isEmpty()) { + mLoginWebView.loadData(customError, "text/html; charset=UTF-8", null); + } + } + }); + } + + private void parseAndLoginFromWebView(String dataString) { + String prefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/"; + LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, dataString); + + if (loginUrlInfo != null) { + mServerInfo.mBaseUrl = AuthenticatorUrlUtils.normalizeUrlSuffix(loginUrlInfo.serverAddress); + webViewUser = loginUrlInfo.username; + webViewPassword = loginUrlInfo.password; + checkOcServer(); + } + } + + private void populateLoginFields(String dataString) throws IllegalArgumentException { + // check if it is cloud://login/ + if (dataString.startsWith(getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/")) { + String prefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/"; + LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, dataString); + + if (loginUrlInfo != null) { + mHostUrlInput.setText(loginUrlInfo.serverAddress); + mUsernameInput.setText(loginUrlInfo.username); + mPasswordInput.setText(loginUrlInfo.password); + + if (loginUrlInfo.serverAddress != null && !mServerIsChecked) { + onUrlInputFocusLost(); + } + } + } + } + + /** + * parses a URI string and returns a login data object with the information from the URI string. + * + * @param prefix URI beginning, e.g. cloud://login/ + * @param dataString the complete URI + * @return login data + * @throws IllegalArgumentException when + */ + public static LoginUrlInfo parseLoginDataUrl(String prefix, String dataString) throws IllegalArgumentException { + if (dataString.length() < prefix.length()) { + throw new IllegalArgumentException("Invalid login URL detected"); + } + LoginUrlInfo loginUrlInfo = new LoginUrlInfo(); + + // format is basically xxx://login/server:xxx&user:xxx&password while all variables are optional + String data = dataString.substring(prefix.length()); + + // parse data + String[] values = data.split("&"); + + if (values.length < 1 || values.length > 3) { + // error illegal number of URL elements detected + throw new IllegalArgumentException("Illegal number of login URL elements detected: " + values.length); + } + + for (String value : values) { + if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { + loginUrlInfo.username = URLDecoder.decode( + value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())); + } else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { + loginUrlInfo.password = URLDecoder.decode( + value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())); + } else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { + loginUrlInfo.serverAddress = URLDecoder.decode( + value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())); + } else { + // error illegal URL element detected + throw new IllegalArgumentException("Illegal magic login URL element detected: " + value); + } + } + + return loginUrlInfo; + } + + private void initAuthTokenType() { + mAuthTokenType = getIntent().getExtras().getString(AccountAuthenticator.KEY_AUTH_TOKEN_TYPE); + if (mAuthTokenType == null) { + if (mAccount != null) { + boolean oAuthRequired = mAccountMgr.getUserData(mAccount, Constants.KEY_SUPPORTS_OAUTH2) != null; + boolean samlWebSsoRequired = ( + mAccountMgr.getUserData + (mAccount, Constants.KEY_SUPPORTS_SAML_WEB_SSO) != null + ); + mAuthTokenType = chooseAuthTokenType(oAuthRequired, samlWebSsoRequired); + + } else { + boolean oAuthSupported = AUTH_ON.equals(getString(R.string.auth_method_oauth2)); + boolean samlWebSsoSupported = AUTH_ON.equals(getString(R.string.auth_method_saml_web_sso)); + mAuthTokenType = chooseAuthTokenType(oAuthSupported, samlWebSsoSupported); + } + } + } + + private String chooseAuthTokenType(boolean oauth, boolean saml) { + if (saml) { + return samlTokenType; + } else if (oauth) { + return oauthTokenType; + } else { + return basicTokenType; + } + } + + + /** + * Configures elements in the user interface under direct control of the Activity. + */ + private void initOverallUi() { + mHostUrlInput = findViewById(R.id.hostUrlInput); + mUsernameInputLayout = findViewById(R.id.input_layout_account_username); + mPasswordInputLayout = findViewById(R.id.input_layout_account_password); + mPasswordInput = findViewById(R.id.account_password); + mUsernameInput = findViewById(R.id.account_username); + mAuthStatusView = findViewById(R.id.auth_status_text); + mOAuth2Check = findViewById(R.id.oauth_onOff_check); + mServerStatusView = findViewById(R.id.server_status_text); + mTestServerButton = findViewById(R.id.testServerButton); + + mOkButton = findViewById(R.id.buttonOK); + mOkButton.setOnClickListener(v -> onOkClick()); + + setupInstructionMessage(); + + mTestServerButton.setVisibility(mAction == ACTION_CREATE ? View.VISIBLE : View.GONE); + } + + + private void setupInstructionMessage() { + String instructionsMessageText = calculateInstructionMessageText(mAction, mAuthTokenType); + TextView instructionsView = findViewById(R.id.instructions_message); + + if (instructionsMessageText != null) { + instructionsView.setVisibility(View.VISIBLE); + instructionsView.setText(instructionsMessageText); + } else { + instructionsView.setVisibility(View.GONE); + } + } + + @Nullable + private String calculateInstructionMessageText(byte action, String authTokenType) { + if (action == ACTION_UPDATE_EXPIRED_TOKEN) { + if (AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType(this)).equals(authTokenType)) { + return getString(R.string.auth_expired_oauth_token_toast); + + } else if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType(this)) + .equals(authTokenType)) { + return getString(R.string.auth_expired_saml_sso_token_toast); + + } else { + return getString(R.string.auth_expired_basic_auth_toast); + } + } + + return null; + } + + public void onTestServerConnectionClick(View v) { + checkOcServer(); + } + + + /** + * @param savedInstanceState Saved activity state, as in {{@link #onCreate(Bundle)} + */ + private void initServerPreFragment(Bundle savedInstanceState) { + + /// step 1 - load and process relevant inputs (resources, intent, savedInstanceState) + boolean isUrlInputAllowed = getResources().getBoolean(R.bool.show_server_url_input); + if (savedInstanceState == null) { + if (mAccount != null) { + mServerInfo.mBaseUrl = mAccountMgr.getUserData(mAccount, Constants.KEY_OC_BASE_URL); + // TODO do next in a setter for mBaseUrl + mServerInfo.mIsSslConn = mServerInfo.mBaseUrl.startsWith(HTTPS_PROTOCOL); + mServerInfo.mVersion = AccountUtils.getServerVersion(mAccount); + } else { + if (!webViewLoginMethod) { + mServerInfo.mBaseUrl = getString(R.string.server_url).trim(); + } else { + mServerInfo.mBaseUrl = getString(R.string.webview_login_url).trim(); + } + mServerInfo.mIsSslConn = mServerInfo.mBaseUrl.startsWith(HTTPS_PROTOCOL); + } + } else { + mServerStatusText = savedInstanceState.getString(KEY_SERVER_STATUS_TEXT); + mServerStatusIcon = savedInstanceState.getInt(KEY_SERVER_STATUS_ICON); + + mServerIsValid = savedInstanceState.getBoolean(KEY_SERVER_VALID); + mServerIsChecked = savedInstanceState.getBoolean(KEY_SERVER_CHECKED); + + // TODO parcelable + mServerInfo.mIsSslConn = savedInstanceState.getBoolean(KEY_IS_SSL_CONN); + mServerInfo.mBaseUrl = savedInstanceState.getString(KEY_HOST_URL_TEXT); + String ocVersion = savedInstanceState.getString(KEY_OC_VERSION); + if (ocVersion != null) { + mServerInfo.mVersion = new OwnCloudVersion(ocVersion); + } + mServerInfo.mAuthMethod = AuthenticationMethod.valueOf( + savedInstanceState.getString(KEY_SERVER_AUTH_METHOD)); + + } + + if (!webViewLoginMethod) { + /// step 2 - set properties of UI elements (text, visibility, enabled...) + mHostUrlInput = findViewById(R.id.hostUrlInput); + // Convert IDN to Unicode + mHostUrlInput.setText(DisplayUtils.convertIdn(mServerInfo.mBaseUrl, false)); + if (mAction != ACTION_CREATE) { + /// lock things that should not change + mHostUrlInput.setEnabled(false); + mHostUrlInput.setFocusable(false); + } + if (isUrlInputAllowed) { + mRefreshButton = findViewById(R.id.embeddedRefreshButton); + } else { + findViewById(R.id.hostUrlFrame).setVisibility(View.GONE); + mRefreshButton = findViewById(R.id.centeredRefreshButton); + } + showRefreshButton(mServerIsChecked && !mServerIsValid && + mWaitingForOpId > Integer.MAX_VALUE); + mServerStatusView = findViewById(R.id.server_status_text); + showServerStatus(); + + /// step 3 - bind some listeners and options + mHostUrlInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + mHostUrlInput.setOnEditorActionListener(this); + + /// step 4 - create listeners that will be bound at onResume + mHostUrlInputWatcher = new TextWatcher() { + + @Override + public void afterTextChanged(Editable s) { + if (mOkButton.isEnabled() && + !mServerInfo.mBaseUrl.equals( + AuthenticatorUrlUtils.normalizeUrl(s.toString(), mServerInfo.mIsSslConn))) { + mOkButton.setEnabled(false); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // not used at the moment + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (mAuthStatusIcon != 0) { + Log_OC.d(TAG, "onTextChanged: hiding authentication status"); + mAuthStatusIcon = 0; + mAuthStatusText = ""; + showAuthStatus(); + } + } + }; + + + // TODO find out if this is really necessary, or if it can done in a different way + findViewById(R.id.scroll).setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN && + AccountTypeUtils.getAuthTokenTypeSamlSessionCookie( + MainApp.getAccountType(getBaseContext())).equals(mAuthTokenType) && + mHostUrlInput.hasFocus()) { + checkOcServer(); + } + return false; + } + }); + } + } + + + /** + * @param savedInstanceState Saved activity state, as in {{@link #onCreate(Bundle)} + */ + private void initAuthorizationPreFragment(Bundle savedInstanceState) { + + /// step 0 - get UI elements in layout + mOAuth2Check = findViewById(R.id.oauth_onOff_check); + mOAuthAuthEndpointText = findViewById(R.id.oAuthEntryPoint_1); + mOAuthTokenEndpointText = findViewById(R.id.oAuthEntryPoint_2); + mUsernameInput = findViewById(R.id.account_username); + mPasswordInput = findViewById(R.id.account_password); + mAuthStatusView = findViewById(R.id.auth_status_text); + + /// step 1 - load and process relevant inputs (resources, intent, savedInstanceState) + String presetUserName = null; + boolean isPasswordExposed = false; + if (savedInstanceState == null) { + if (mAccount != null) { + presetUserName = com.owncloud.android.lib.common.accounts.AccountUtils.getUsernameForAccount(mAccount); + } + } else { + isPasswordExposed = savedInstanceState.getBoolean(KEY_PASSWORD_EXPOSED, false); + mAuthStatusText = savedInstanceState.getString(KEY_AUTH_STATUS_TEXT); + mAuthStatusIcon = savedInstanceState.getInt(KEY_AUTH_STATUS_ICON); + mAuthToken = savedInstanceState.getString(KEY_AUTH_TOKEN); + } + + /// step 2 - set properties of UI elements (text, visibility, enabled...) + mOAuth2Check.setChecked( + AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType(this)).equals(mAuthTokenType)); + if (presetUserName != null) { + mUsernameInput.setText(presetUserName); + } + if (mAction != ACTION_CREATE) { + mUsernameInput.setEnabled(false); + mUsernameInput.setFocusable(false); + } + mPasswordInput.setText(""); // clean password to avoid social hacking + if (isPasswordExposed) { + showPassword(); + } + updateAuthenticationPreFragmentVisibility(); + showAuthStatus(); + mOkButton.setEnabled(mServerIsValid); + + + /// step 3 - bind listeners + // bindings for password input field + mPasswordInput.setOnFocusChangeListener(this); + mPasswordInput.setImeOptions(EditorInfo.IME_ACTION_DONE); + mPasswordInput.setOnEditorActionListener(this); + mPasswordInput.setOnTouchListener(new RightDrawableOnTouchListener() { + @Override + public boolean onDrawableTouch(final MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP) { + AuthenticatorActivity.this.onViewPasswordClick(); + } + return true; + } + }); + + } + + + /** + * Changes the visibility of input elements depending on + * the current authorization method. + */ + private void updateAuthenticationPreFragmentVisibility() { + if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType(this)).equals(mAuthTokenType)) { + // SAML-based web Single Sign On + mOAuth2Check.setVisibility(View.GONE); + mOAuthAuthEndpointText.setVisibility(View.GONE); + mOAuthTokenEndpointText.setVisibility(View.GONE); + mUsernameInput.setVisibility(View.GONE); + mPasswordInput.setVisibility(View.GONE); + + } else { + if (mAction == ACTION_CREATE && + AUTH_OPTIONAL.equals(getString(R.string.auth_method_oauth2))) { + mOAuth2Check.setVisibility(View.VISIBLE); + } else { + mOAuth2Check.setVisibility(View.GONE); + } + + if (AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType(this)).equals(mAuthTokenType)) { + // OAuth 2 authorization + mOAuthAuthEndpointText.setVisibility(View.VISIBLE); + mOAuthTokenEndpointText.setVisibility(View.VISIBLE); + mUsernameInput.setVisibility(View.GONE); + mPasswordInput.setVisibility(View.GONE); + + } else { + // basic HTTP authorization + mOAuthAuthEndpointText.setVisibility(View.GONE); + mOAuthTokenEndpointText.setVisibility(View.GONE); + mUsernameInput.setVisibility(View.VISIBLE); + mPasswordInput.setVisibility(View.VISIBLE); + } + } + } + + + /** + * Saves relevant state before {@link #onPause()} + * + * Do NOT save {@link #mNewCapturedUriFromOAuth2Redirection}; it keeps a temporal flag, + * intended to defer the processing of the redirection caught in + * {@link #onNewIntent(Intent)} until {@link #onResume()} + * + * See {@link super#onSaveInstanceState(Bundle)} + */ + @Override + protected void onSaveInstanceState(Bundle outState) { + //Log_OC.e(TAG, "onSaveInstanceState init" ); + super.onSaveInstanceState(outState); + + /// global state + outState.putString(KEY_AUTH_TOKEN_TYPE, mAuthTokenType); + outState.putLong(KEY_WAITING_FOR_OP_ID, mWaitingForOpId); + + if (!webViewLoginMethod) { + /// Server PRE-fragment state + outState.putString(KEY_SERVER_STATUS_TEXT, mServerStatusText); + outState.putInt(KEY_SERVER_STATUS_ICON, mServerStatusIcon); + outState.putBoolean(KEY_SERVER_CHECKED, mServerIsChecked); + outState.putBoolean(KEY_SERVER_VALID, mServerIsValid); + + /// Authentication PRE-fragment state + outState.putBoolean(KEY_PASSWORD_EXPOSED, isPasswordVisible()); + outState.putInt(KEY_AUTH_STATUS_ICON, mAuthStatusIcon); + outState.putString(KEY_AUTH_STATUS_TEXT, mAuthStatusText); + outState.putString(KEY_AUTH_TOKEN, mAuthToken); + } + + outState.putBoolean(KEY_IS_SSL_CONN, mServerInfo.mIsSslConn); + outState.putString(KEY_HOST_URL_TEXT, mServerInfo.mBaseUrl); + if (mServerInfo.mVersion != null) { + outState.putString(KEY_OC_VERSION, mServerInfo.mVersion.getVersion()); + } + outState.putString(KEY_SERVER_AUTH_METHOD, mServerInfo.mAuthMethod.name()); + + /// authentication + outState.putBoolean(KEY_AUTH_IS_FIRST_ATTEMPT_TAG, mIsFirstAuthAttempt); + + /// AsyncTask (User and password) + if (!webViewLoginMethod) { + outState.putString(KEY_USERNAME, mUsernameInput.getText().toString().trim()); + outState.putString(KEY_PASSWORD, mPasswordInput.getText().toString()); + } + + if (mAsyncTask != null) { + mAsyncTask.cancel(true); + outState.putBoolean(KEY_ASYNC_TASK_IN_PROGRESS, true); + } else { + outState.putBoolean(KEY_ASYNC_TASK_IN_PROGRESS, false); + } + mAsyncTask = null; + + //Log_OC.e(TAG, "onSaveInstanceState end" ); + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + mServerIsChecked = savedInstanceState.getBoolean(KEY_SERVER_CHECKED, false); + + // AsyncTask + boolean inProgress = savedInstanceState.getBoolean(KEY_ASYNC_TASK_IN_PROGRESS); + if (inProgress) { + String username = savedInstanceState.getString(KEY_USERNAME); + String password = savedInstanceState.getString(KEY_PASSWORD); + + OwnCloudCredentials credentials = null; + if (basicTokenType.equals(mAuthTokenType)) { + credentials = OwnCloudCredentialsFactory.newBasicCredentials(username, password); + + } else if (oauthTokenType.equals(mAuthTokenType)) { + credentials = OwnCloudCredentialsFactory.newBearerCredentials(mAuthToken); + + } + accessRootFolder(credentials); + } + } + + /** + * The redirection triggered by the OAuth authentication server as response to the + * GET AUTHORIZATION request is caught here. + * + * To make this possible, this activity needs to be qualified with android:launchMode = + * "singleTask" in the AndroidManifest.xml file. + */ + @Override + protected void onNewIntent(Intent intent) { + Log_OC.d(TAG, "onNewIntent()"); + Uri data = intent.getData(); + if (data != null && data.toString().startsWith(getString(R.string.oauth2_redirect_uri))) { + mNewCapturedUriFromOAuth2Redirection = data; + } + + if (intent.getBooleanExtra(EXTRA_USE_PROVIDER_AS_WEBLOGIN, false)) { + webViewLoginMethod = true; + setContentView(R.layout.account_setup_webview); + mLoginWebView = findViewById(R.id.login_webview); + initWebViewLogin(getString(R.string.provider_registration_server), false); + } + } + + + /** + * The redirection triggered by the OAuth authentication server as response to the + * GET AUTHORIZATION, and deferred in {@link #onNewIntent(Intent)}, is processed here. + */ + @Override + protected void onResume() { + super.onResume(); + + if (!webViewLoginMethod) { + // bound here to avoid spurious changes triggered by Android on device rotations + mHostUrlInput.setOnFocusChangeListener(this); + mHostUrlInput.addTextChangedListener(mHostUrlInputWatcher); + + if (mNewCapturedUriFromOAuth2Redirection != null) { + getOAuth2AccessTokenFromCapturedRedirection(); + } + + String dataString = getIntent().getDataString(); + if (dataString != null) { + try { + populateLoginFields(dataString); + } catch (IllegalArgumentException e) { + DisplayUtils.showSnackMessage(findViewById(R.id.scroll), R.string.auth_illegal_login_used); + Log_OC.e(TAG, "Illegal login data URL used, no Login pre-fill!", e); + } + } + } + + // bind to Operations Service + mOperationsServiceConnection = new OperationsServiceConnection(); + if (!bindService(new Intent(this, OperationsService.class), + mOperationsServiceConnection, + Context.BIND_AUTO_CREATE)) { + DisplayUtils.showSnackMessage(findViewById(R.id.scroll), R.string.error_cant_bind_to_operations_service); + finish(); + } + + if (mOperationsServiceBinder != null) { + doOnResumeAndBound(); + } + } + + + @Override + protected void onPause() { + if (mOperationsServiceBinder != null) { + mOperationsServiceBinder.removeOperationListener(this); + } + + if (!webViewLoginMethod) { + mHostUrlInput.removeTextChangedListener(mHostUrlInputWatcher); + mHostUrlInput.setOnFocusChangeListener(null); + } + + super.onPause(); + } + + @Override + protected void onDestroy() { + + mHostUrlInputWatcher = null; + + if (mOperationsServiceConnection != null) { + unbindService(mOperationsServiceConnection); + mOperationsServiceBinder = null; + } + + if (webViewLoginMethod) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR); + } + + super.onDestroy(); + } + + /** + * Parses the redirection with the response to the GET AUTHORIZATION request to the + * oAuth server and requests for the access token (GET ACCESS TOKEN) + */ + private void getOAuth2AccessTokenFromCapturedRedirection() { + /// Parse data from OAuth redirection + String queryParameters = mNewCapturedUriFromOAuth2Redirection.getQuery(); + mNewCapturedUriFromOAuth2Redirection = null; + + /// Showing the dialog with instructions for the user. + IndeterminateProgressDialog dialog = + IndeterminateProgressDialog.newInstance(R.string.auth_getting_authorization, true); + dialog.show(getSupportFragmentManager(), WAIT_DIALOG_TAG); + + /// GET ACCESS TOKEN to the oAuth server + Intent getServerInfoIntent = new Intent(); + getServerInfoIntent.setAction(OperationsService.ACTION_OAUTH2_GET_ACCESS_TOKEN); + + getServerInfoIntent.putExtra( + OperationsService.EXTRA_SERVER_URL, + mOAuthTokenEndpointText.getText().toString().trim()); + + getServerInfoIntent.putExtra( + OperationsService.EXTRA_OAUTH2_QUERY_PARAMETERS, + queryParameters); + + if (mOperationsServiceBinder != null) { + //Log_OC.e(TAG, "getting access token..." ); + mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getServerInfoIntent); + } + } + + + /** + * Handles the change of focus on the text inputs for the server URL and the password + */ + public void onFocusChange(View view, boolean hasFocus) { + if (view.getId() == R.id.hostUrlInput) { + if (!hasFocus) { + onUrlInputFocusLost(); + } else { + showRefreshButton(false); + } + + } else if (view.getId() == R.id.account_password) { + onPasswordFocusChanged(hasFocus); + } + } + + + /** + * Handles changes in focus on the text input for the server URL. + * + * IMPORTANT ENTRY POINT 2: When (!hasFocus), user wrote the server URL and changed to + * other field. The operation to check the existence of the server in the entered URL is + * started. + * + * When hasFocus: user 'comes back' to write again the server URL. + */ + private void onUrlInputFocusLost() { + if (!mServerInfo.mBaseUrl.equals( + AuthenticatorUrlUtils.normalizeUrl(mHostUrlInput.getText().toString(), mServerInfo.mIsSslConn))) { + // check server again only if the user changed something in the field + checkOcServer(); + } else { + mOkButton.setEnabled(mServerIsValid); + showRefreshButton(!mServerIsValid); + } + } + + + private void checkOcServer() { + String uri; + if (mHostUrlInput != null && !mHostUrlInput.getText().toString().isEmpty()) { + uri = mHostUrlInput.getText().toString().trim(); + mOkButton.setEnabled(false); + showRefreshButton(false); + } else { + uri = mServerInfo.mBaseUrl; + } + + mServerIsValid = false; + mServerIsChecked = false; + mServerInfo = new GetServerInfoOperation.ServerInfo(); + + if (uri.length() != 0) { + if (mHostUrlInput != null) { + uri = AuthenticatorUrlUtils.stripIndexPhpOrAppsFiles(uri); + mHostUrlInput.setText(uri); + } + + // Handle internationalized domain names + try { + uri = DisplayUtils.convertIdn(uri, true); + } catch (IllegalArgumentException ex) { + // Let Owncloud library check the error of the malformed URI + Log_OC.e(TAG, "Error converting internationalized domain name " + uri, ex); + } + + if (mHostUrlInput != null) { + mServerStatusText = getResources().getString(R.string.auth_testing_connection); + mServerStatusIcon = R.drawable.progress_small; + showServerStatus(); + } + + Intent getServerInfoIntent = new Intent(); + getServerInfoIntent.setAction(OperationsService.ACTION_GET_SERVER_INFO); + getServerInfoIntent.putExtra(OperationsService.EXTRA_SERVER_URL, + AuthenticatorUrlUtils.normalizeUrlSuffix(uri)); + + if (mOperationsServiceBinder != null) { + mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getServerInfoIntent); + } else { + Log_OC.e(TAG, "Server check tried with OperationService unbound!"); + } + + } else { + mServerStatusText = ""; + mServerStatusIcon = 0; + if (!webViewLoginMethod) { + showServerStatus(); + } + } + } + + + /** + * Handles changes in focus on the text input for the password (basic authorization). + * + * When (hasFocus), the button to toggle password visibility is shown. + * + * When (!hasFocus), the button is made invisible and the password is hidden. + * + * @param hasFocus 'True' if focus is received, 'false' if is lost + */ + + private void onPasswordFocusChanged(boolean hasFocus) { + if (hasFocus) { + showViewPasswordButton(); + } else { + hidePassword(); + hidePasswordButton(); + } + } + + + private void showViewPasswordButton() { + int drawable = R.drawable.ic_view; + if (isPasswordVisible()) { + drawable = R.drawable.ic_hide; + } + mPasswordInput.setCompoundDrawablesWithIntrinsicBounds(0, 0, drawable, 0); + } + + private boolean isPasswordVisible() { + return ((mPasswordInput.getInputType() & InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) == + InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); + } + + private void hidePasswordButton() { + mPasswordInput.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + } + + private void showPassword() { + mPasswordInput.setInputType( + InputType.TYPE_CLASS_TEXT | + InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + ); + showViewPasswordButton(); + } + + private void hidePassword() { + mPasswordInput.setInputType( + InputType.TYPE_CLASS_TEXT | + InputType.TYPE_TEXT_VARIATION_PASSWORD | + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + ); + showViewPasswordButton(); + } + + /** + * Checks the credentials of the user in the root of the ownCloud server + * before creating a new local account. + * + * For basic authorization, a check of existence of the root folder is + * performed. + * + * For OAuth, starts the flow to get an access token; the credentials test + * is postponed until it is available. + * + * IMPORTANT ENTRY POINT 4 + */ + public void onOkClick() { + // this check should be unnecessary + if (mServerInfo.mVersion == null || + !mServerInfo.mVersion.isVersionValid() || + mServerInfo.mBaseUrl == null || + mServerInfo.mBaseUrl.length() == 0) { + mServerStatusIcon = R.drawable.ic_alert; + mServerStatusText = getResources().getString(R.string.auth_wtf_reenter_URL); + showServerStatus(); + mOkButton.setEnabled(false); + return; + } + + if (AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.getAccountType(this)).equals(mAuthTokenType)) { + startOauthorization(); + } else if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType(this)) + .equals(mAuthTokenType)) { + startSamlBasedFederatedSingleSignOnAuthorization(); + } else { + checkBasicAuthorization(null, null); + } + } + + + /** + * Tests the credentials entered by the user performing a check of existence on + * the root folder of the ownCloud server. + */ + private void checkBasicAuthorization(@Nullable String webViewUsername, @Nullable String webViewPassword) { + /// get basic credentials entered by user + String username; + String password; + if (!webViewLoginMethod) { + username = mUsernameInput.getText().toString().trim(); + password = mPasswordInput.getText().toString(); + } else { + username = webViewUsername; + password = webViewPassword; + } + + /// be gentle with the user + IndeterminateProgressDialog dialog = + IndeterminateProgressDialog.newInstance(R.string.auth_trying_to_login, true); + dialog.show(getSupportFragmentManager(), WAIT_DIALOG_TAG); + + /// validate credentials accessing the root folder + OwnCloudCredentials credentials = OwnCloudCredentialsFactory.newBasicCredentials(username, password); + accessRootFolder(credentials); + } + + private void accessRootFolder(OwnCloudCredentials credentials) { + mAsyncTask = new AuthenticatorAsyncTask(this); + Object[] params = {mServerInfo.mBaseUrl, credentials}; + mAsyncTask.execute(params); + } + + + /** + * Starts the OAuth 'grant type' flow to get an access token, with + * a GET AUTHORIZATION request to the BUILT-IN authorization server. + */ + private void startOauthorization() { + // be gentle with the user + mAuthStatusIcon = R.drawable.progress_small; + mAuthStatusText = getResources().getString(R.string.oauth_login_connection); + showAuthStatus(); + + // GET AUTHORIZATION request + Uri uri = Uri.parse(mOAuthAuthEndpointText.getText().toString().trim()); + Uri.Builder uriBuilder = uri.buildUpon(); + uriBuilder.appendQueryParameter( + OAuth2Constants.KEY_RESPONSE_TYPE, getString(R.string.oauth2_response_type) + ); + uriBuilder.appendQueryParameter( + OAuth2Constants.KEY_REDIRECT_URI, getString(R.string.oauth2_redirect_uri) + ); + uriBuilder.appendQueryParameter( + OAuth2Constants.KEY_CLIENT_ID, getString(R.string.oauth2_client_id) + ); + uriBuilder.appendQueryParameter( + OAuth2Constants.KEY_SCOPE, getString(R.string.oauth2_scope) + ); + uri = uriBuilder.build(); + Log_OC.d(TAG, "Starting browser to view " + uri.toString()); + Intent i = new Intent(Intent.ACTION_VIEW, uri); + startActivity(i); + } + + + /** + * Starts the Web Single Sign On flow to get access to the root folder + * in the server. + */ + private void startSamlBasedFederatedSingleSignOnAuthorization() { + /// be gentle with the user + mAuthStatusIcon = R.drawable.progress_small; + mAuthStatusText = getResources().getString(R.string.auth_connecting_auth_server); + showAuthStatus(); + + /// Show SAML-based SSO web dialog + String targetUrl = mServerInfo.mBaseUrl + + AuthenticatorUrlUtils.getWebdavPath(mServerInfo.mVersion, mAuthTokenType, this); + SamlWebViewDialog dialog = SamlWebViewDialog.newInstance(targetUrl, targetUrl); + dialog.show(getSupportFragmentManager(), SAML_DIALOG_TAG); + } + + /** + * Callback method invoked when a RemoteOperation executed by this Activity finishes. + * + * Dispatches the operation flow to the right method. + */ + @Override + public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) { + + if (operation instanceof GetServerInfoOperation) { + if (operation.hashCode() == mWaitingForOpId) { + onGetServerInfoFinish(result); + } // else nothing ; only the last check operation is considered; + // multiple can be started if the user amends a URL quickly + + } else if (operation instanceof OAuth2GetAccessToken) { + onGetOAuthAccessTokenFinish(result); + + } else if (operation instanceof GetRemoteUserInfoOperation) { + onGetUserNameFinish(result); + } + + } + + private void onGetUserNameFinish(RemoteOperationResult result) { + mWaitingForOpId = Long.MAX_VALUE; + if (result.isSuccess()) { + boolean success = false; + String username; + if (result.getData().get(0) instanceof UserInfo) { + username = ((UserInfo) result.getData().get(0)).getDisplayName(); + } else { + username = (String) result.getData().get(0); + } + + if (mAction == ACTION_CREATE) { + if (!webViewLoginMethod) { + mUsernameInput.setText(username); + } + success = createAccount(result); + } else { + + if (!webViewLoginMethod && !mUsernameInput.getText().toString().trim().equals(username)) { + // fail - not a new account, but an existing one; disallow + result = new RemoteOperationResult(ResultCode.ACCOUNT_NOT_THE_SAME); + mAuthToken = ""; + updateAuthStatusIconAndText(result); + showAuthStatus(); + Log_OC.d(TAG, result.getLogMessage()); + } else { + try { + updateAccountAuthentication(); + success = true; + + } catch (AccountNotFoundException e) { + Log_OC.e(TAG, "Account " + mAccount + " was removed!", e); + DisplayUtils.showSnackMessage(findViewById(R.id.scroll), R.string.auth_account_does_not_exist); + finish(); + } + } + } + + if (success) { + finish(); + } + } else { + if (!webViewLoginMethod) { + int statusText = result.getCode() == ResultCode.MAINTENANCE_MODE ? R.string.maintenance_mode : R.string.auth_fail_get_user_name; + updateStatusIconFailUserName(statusText); + showAuthStatus(); + } + Log_OC.e(TAG, "Access to user name failed: " + result.getLogMessage()); + } + + } + + /** + * Processes the result of the server check performed when the user finishes the enter of the + * server URL. + * + * @param result Result of the check. + */ + private void onGetServerInfoFinish(RemoteOperationResult result) { + /// update activity state + mServerIsChecked = true; + mWaitingForOpId = Long.MAX_VALUE; + + // update server status, but don't show it yet + if (!webViewLoginMethod) { + updateServerStatusIconAndText(result); + } + + if (result.isSuccess()) { + /// SUCCESS means: + // 1. connection succeeded, and we know if it's SSL or not + // 2. server is installed + // 3. we got the server version + // 4. we got the authentication method required by the server + mServerInfo = (GetServerInfoOperation.ServerInfo) (result.getData().get(0)); + + // show outdated warning + if (getResources().getBoolean(R.bool.show_outdated_server_warning) && + mServerInfo.mVersion.compareTo(MainApp.OUTDATED_SERVER_VERSION) < 0) { + DisplayUtils.showServerOutdatedSnackbar(this); + } + + webViewLoginMethod = mServerInfo.mVersion.isWebLoginSupported() && !forceOldLoginMethod; + + if (webViewUser != null && !webViewUser.isEmpty() && + webViewPassword != null && !webViewPassword.isEmpty()) { + checkBasicAuthorization(webViewUser, webViewPassword); + } else if (webViewLoginMethod) { + // hide old login + setOldLoginVisibility(View.GONE); + + setContentView(R.layout.account_setup_webview); + mLoginWebView = findViewById(R.id.login_webview); + initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, true); + } else { + // show old login + setOldLoginVisibility(View.VISIBLE); + } + + if (!authSupported(mServerInfo.mAuthMethod)) { + + if (!webViewLoginMethod) { + // overrides updateServerStatusIconAndText() + updateServerStatusIconNoRegularAuth(); + } + mServerIsValid = false; + + } else { + mServerIsValid = true; + } + + } else { + mServerIsValid = false; + } + + // refresh UI + if (!webViewLoginMethod) { + showRefreshButton(!mServerIsValid); + showServerStatus(); + mOkButton.setEnabled(mServerIsValid); + } + + if (!mServerIsValid) { + // hide old login + setOldLoginVisibility(View.GONE); + } + + /// very special case (TODO: move to a common place for all the remote operations) + if (result.getCode() == ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED) { + showUntrustedCertDialog(result); + } + } + + private void setOldLoginVisibility(int visible) { + mOkButton.setVisibility(visible); + mUsernameInputLayout.setVisibility(visible); + mPasswordInputLayout.setVisibility(visible); + } + + private boolean authSupported(AuthenticationMethod authMethod) { + return ((basicTokenType.equals(mAuthTokenType) && + AuthenticationMethod.BASIC_HTTP_AUTH.equals(authMethod)) || + (oauthTokenType.equals(mAuthTokenType) && + AuthenticationMethod.BEARER_TOKEN.equals(authMethod)) || + (samlTokenType.equals(mAuthTokenType) && + AuthenticationMethod.SAML_WEB_SSO.equals(authMethod)) + ); + } + + /** + * Chooses the right icon and text to show to the user for the received operation result. + * + * @param result Result of a remote operation performed in this activity + */ + private void updateServerStatusIconAndText(RemoteOperationResult result) { + mServerStatusIcon = R.drawable.ic_alert; // the most common case in the switch below + + switch (result.getCode()) { + case OK_SSL: + mServerStatusIcon = R.drawable.ic_lock_white; + mServerStatusText = getResources().getString(R.string.auth_secure_connection); + break; + + case OK_NO_SSL: + case OK: + if (mHostUrlInput.getText().toString().trim().toLowerCase(Locale.ROOT).startsWith(HTTP_PROTOCOL)) { + mServerStatusText = getResources().getString(R.string.auth_connection_established); + mServerStatusIcon = R.drawable.ic_ok; + } else { + mServerStatusText = getResources().getString(R.string.auth_nossl_plain_ok_title); + mServerStatusIcon = R.drawable.ic_lock_open_white; + } + break; + + case NO_NETWORK_CONNECTION: + mServerStatusIcon = R.drawable.no_network; + mServerStatusText = getResources().getString(R.string.auth_no_net_conn_title); + break; + + case SSL_RECOVERABLE_PEER_UNVERIFIED: + mServerStatusText = getResources().getString(R.string.auth_ssl_unverified_server_title); + break; + case BAD_OC_VERSION: + mServerStatusText = getResources().getString(R.string.auth_bad_oc_version_title); + break; + case WRONG_CONNECTION: + mServerStatusText = getResources().getString(R.string.auth_wrong_connection_title); + break; + case TIMEOUT: + mServerStatusText = getResources().getString(R.string.auth_timeout_title); + break; + case INCORRECT_ADDRESS: + mServerStatusText = getResources().getString(R.string.auth_incorrect_address_title); + break; + case SSL_ERROR: + mServerStatusText = getResources().getString(R.string.auth_ssl_general_error_title); + break; + case UNAUTHORIZED: + mServerStatusText = getResources().getString(R.string.auth_unauthorized); + break; + case HOST_NOT_AVAILABLE: + mServerStatusText = getResources().getString(R.string.auth_unknown_host_title); + break; + case INSTANCE_NOT_CONFIGURED: + mServerStatusText = getResources().getString(R.string.auth_not_configured_title); + break; + case FILE_NOT_FOUND: + mServerStatusText = getResources().getString(R.string.auth_incorrect_path_title); + break; + case OAUTH2_ERROR: + mServerStatusText = getResources().getString(R.string.auth_oauth_error); + break; + case OAUTH2_ERROR_ACCESS_DENIED: + mServerStatusText = getResources().getString(R.string.auth_oauth_error_access_denied); + break; + case UNHANDLED_HTTP_CODE: + mServerStatusText = getResources().getString(R.string.auth_unknown_error_http_title); + break; + case UNKNOWN_ERROR: + if (result.getException() != null && + !TextUtils.isEmpty(result.getException().getMessage())) { + mServerStatusText = getResources().getString( + R.string.auth_unknown_error_exception_title, + result.getException().getMessage() + ); + } else { + mServerStatusText = getResources().getString(R.string.auth_unknown_error_title); + } + break; + case OK_REDIRECT_TO_NON_SECURE_CONNECTION: + mServerStatusIcon = R.drawable.ic_lock_open_white; + mServerStatusText = getResources().getString(R.string.auth_redirect_non_secure_connection_title); + break; + case MAINTENANCE_MODE: + mServerStatusText = getResources().getString(R.string.maintenance_mode); + break; + case UNTRUSTED_DOMAIN: + mServerStatusText = getResources().getString(R.string.untrusted_domain); + break; + default: + mServerStatusText = ""; + mServerStatusIcon = 0; + break; + } + } + + + /** + * Chooses the right icon and text to show to the user for the received operation result. + * + * @param result Result of a remote operation performed in this activity + */ + private void updateAuthStatusIconAndText(RemoteOperationResult result) { + mAuthStatusIcon = R.drawable.ic_alert; // the most common case in the switch below + + switch (result.getCode()) { + case OK_SSL: + mAuthStatusIcon = R.drawable.ic_lock_white; + mAuthStatusText = getResources().getString(R.string.auth_secure_connection); + break; + + case OK_NO_SSL: + case OK: + if (mHostUrlInput.getText().toString().trim().toLowerCase(Locale.ROOT).startsWith(HTTP_PROTOCOL)) { + mAuthStatusText = getResources().getString(R.string.auth_connection_established); + mAuthStatusIcon = R.drawable.ic_ok; + } else { + mAuthStatusText = getResources().getString(R.string.auth_nossl_plain_ok_title); + mAuthStatusIcon = R.drawable.ic_lock_open_white; + } + break; + + case NO_NETWORK_CONNECTION: + mAuthStatusIcon = R.drawable.no_network; + mAuthStatusText = getResources().getString(R.string.auth_no_net_conn_title); + break; + + case SSL_RECOVERABLE_PEER_UNVERIFIED: + mAuthStatusText = getResources().getString(R.string.auth_ssl_unverified_server_title); + break; + case TIMEOUT: + mAuthStatusText = getResources().getString(R.string.auth_timeout_title); + break; + case HOST_NOT_AVAILABLE: + mAuthStatusText = getResources().getString(R.string.auth_unknown_host_title); + break; + case UNHANDLED_HTTP_CODE: + default: + mAuthStatusText = ErrorMessageAdapter.getErrorCauseMessage(result, null, getResources()); + } + } + + private void updateStatusIconFailUserName(int failedStatusText) { + mAuthStatusIcon = R.drawable.ic_alert; + mAuthStatusText = getResources().getString(failedStatusText); + } + + private void updateServerStatusIconNoRegularAuth() { + mServerStatusIcon = R.drawable.ic_alert; + mServerStatusText = getResources().getString(R.string.auth_can_not_auth_against_server); + } + + /** + * Processes the result of the request for and access token send + * to an OAuth authorization server. + * + * @param result Result of the operation. + */ + private void onGetOAuthAccessTokenFinish(RemoteOperationResult result) { + mWaitingForOpId = Long.MAX_VALUE; + dismissDialog(WAIT_DIALOG_TAG); + + if (result.isSuccess()) { + /// be gentle with the user + IndeterminateProgressDialog dialog = + IndeterminateProgressDialog.newInstance(R.string.auth_trying_to_login, true); + dialog.show(getSupportFragmentManager(), WAIT_DIALOG_TAG); + + /// time to test the retrieved access token on the ownCloud server + @SuppressWarnings("unchecked") + Map tokens = (Map) (result.getData().get(0)); + mAuthToken = tokens.get(OAuth2Constants.KEY_ACCESS_TOKEN); + Log_OC.d(TAG, "Got ACCESS TOKEN: " + mAuthToken); + + /// validate token accessing to root folder / getting session + OwnCloudCredentials credentials = OwnCloudCredentialsFactory.newBearerCredentials( + mAuthToken); + accessRootFolder(credentials); + + } else { + updateAuthStatusIconAndText(result); + showAuthStatus(); + Log_OC.d(TAG, "Access failed: " + result.getLogMessage()); + } + } + + + /** + * Processes the result of the access check performed to try the user credentials. + * + * Creates a new account through the AccountManager. + * + * @param result Result of the operation. + */ + @Override + public void onAuthenticatorTaskCallback(RemoteOperationResult result) { + mWaitingForOpId = Long.MAX_VALUE; + dismissDialog(WAIT_DIALOG_TAG); + mAsyncTask = null; + + if (result.isSuccess()) { + Log_OC.d(TAG, "Successful access - time to save the account"); + + boolean success = false; + + if (mAction == ACTION_CREATE) { + success = createAccount(result); + + } else { + try { + updateAccountAuthentication(); + success = true; + + } catch (AccountNotFoundException e) { + Log_OC.e(TAG, "Account " + mAccount + " was removed!", e); + DisplayUtils.showSnackMessage(findViewById(R.id.scroll), R.string.auth_account_does_not_exist); + finish(); + } + } + + // Reset webView + webViewPassword = null; + webViewUser = null; + forceOldLoginMethod = false; + deleteCookies(); + + if (success) { + finish(); + } else { + // init webView again + if (mLoginWebView != null) { + mLoginWebView.setVisibility(View.GONE); + } + setContentView(R.layout.account_setup); + + initOverallUi(); + + CustomEditText serverAddressField = findViewById(R.id.hostUrlInput); + serverAddressField.setText(mServerInfo.mBaseUrl); + + findViewById(R.id.oauth_onOff_check).setVisibility(View.GONE); + findViewById(R.id.server_status_text).setVisibility(View.GONE); + mAuthStatusView = findViewById(R.id.auth_status_text); + + showAuthStatus(); + } + + } else if (result.isServerFail() || result.isException()) { + /// server errors or exceptions in authorization take to requiring a new check of + /// the server + mServerIsChecked = true; + mServerIsValid = false; + mServerInfo = new GetServerInfoOperation.ServerInfo(); + + // update status icon and text + updateServerStatusIconAndText(result); + showServerStatus(); + mAuthStatusIcon = 0; + mAuthStatusText = ""; + if (!webViewLoginMethod) { + showAuthStatus(); + + // update input controls state + showRefreshButton(true); + mOkButton.setEnabled(false); + } + + // very special case (TODO: move to a common place for all the remote operations) + if (result.getCode() == ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED) { + showUntrustedCertDialog(result); + } + + } else { // authorization fail due to client side - probably wrong credentials + if (webViewLoginMethod) { + mLoginWebView = findViewById(R.id.login_webview); + initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, true); + + DisplayUtils.showSnackMessage(this, mLoginWebView, R.string.auth_access_failed, result.getLogMessage()); + } else { + updateAuthStatusIconAndText(result); + showAuthStatus(); + } + // reset webview + webViewPassword = null; + webViewUser = null; + deleteCookies(); + + Log_OC.d(TAG, "Access failed: " + result.getLogMessage()); + } + } + + + /** + * Updates the authentication token. + * + * Sets the proper response so that the AccountAuthenticator that started this activity + * saves a new authorization token for mAccount. + * + * Kills the session kept by OwnCloudClientManager so that a new one will created with + * the new credentials when needed. + */ + private void updateAccountAuthentication() throws AccountNotFoundException { + String accountType = MainApp.getAccountType(this); + + Bundle response = new Bundle(); + response.putString(AccountManager.KEY_ACCOUNT_NAME, mAccount.name); + response.putString(AccountManager.KEY_ACCOUNT_TYPE, mAccount.type); + + if (AccountTypeUtils.getAuthTokenTypeAccessToken(accountType).equals(mAuthTokenType)) { + response.putString(AccountManager.KEY_AUTHTOKEN, mAuthToken); + // the next line is necessary, notifications are calling directly to the + // AuthenticatorActivity to update, without AccountManager intervention + mAccountMgr.setAuthToken(mAccount, mAuthTokenType, mAuthToken); + + } else if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(accountType).equals(mAuthTokenType)) { + response.putString(AccountManager.KEY_AUTHTOKEN, mAuthToken); + // the next line is necessary; by now, notifications are calling directly to the + // AuthenticatorActivity to update, without AccountManager intervention + mAccountMgr.setAuthToken(mAccount, mAuthTokenType, mAuthToken); + + } else { + if (!webViewLoginMethod) { + response.putString(AccountManager.KEY_AUTHTOKEN, mPasswordInput.getText().toString()); + mAccountMgr.setPassword(mAccount, mPasswordInput.getText().toString()); + } else { + response.putString(AccountManager.KEY_AUTHTOKEN, webViewPassword); + mAccountMgr.setPassword(mAccount, webViewPassword); + } + } + + // remove managed clients for this account to enforce creation with fresh credentials + OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, this); + OwnCloudClientManagerFactory.getDefaultSingleton().removeClientFor(ocAccount); + + setAccountAuthenticatorResult(response); + final Intent intent = new Intent(); + intent.putExtras(response); + setResult(RESULT_OK, intent); + + } + + + /** + * Creates a new account through the Account Authenticator that started this activity. + * + * This makes the account permanent. + * + * TODO Decide how to name the OAuth accounts + */ + @SuppressFBWarnings("DMI") + private boolean createAccount(RemoteOperationResult authResult) { + String accountType = MainApp.getAccountType(this); + + // create and save new ownCloud account + boolean isOAuth = AccountTypeUtils.getAuthTokenTypeAccessToken(accountType).equals(mAuthTokenType); + boolean isSaml = AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(accountType).equals(mAuthTokenType); + + String lastPermanentLocation = authResult.getLastPermanentLocation(); + if (lastPermanentLocation != null) { + mServerInfo.mBaseUrl = AuthenticatorUrlUtils.trimWebdavSuffix(lastPermanentLocation); + } + + Uri uri = Uri.parse(mServerInfo.mBaseUrl); + String username; + if (!webViewLoginMethod) { + username = mUsernameInput.getText().toString().trim(); + } else { + username = webViewUser; + } + if (isOAuth) { + username = "OAuth_user" + (new java.util.Random(System.currentTimeMillis())).nextLong(); + } + + String accountName = com.owncloud.android.lib.common.accounts.AccountUtils.buildAccountName(uri, username); + Account newAccount = new Account(accountName, accountType); + if (AccountUtils.exists(newAccount, getApplicationContext())) { + // fail - not a new account, but an existing one; disallow + RemoteOperationResult result = new RemoteOperationResult(ResultCode.ACCOUNT_NOT_NEW); + + updateAuthStatusIconAndText(result); + showAuthStatus(); + + Log_OC.d(TAG, result.getLogMessage()); + return false; + + } else { + mAccount = newAccount; + + if (isOAuth || isSaml) { + // with external authorizations, the password is never input in the app + mAccountMgr.addAccountExplicitly(mAccount, "", null); + } else { + if (!webViewLoginMethod) { + mAccountMgr.addAccountExplicitly(mAccount, mPasswordInput.getText().toString(), null); + } else { + mAccountMgr.addAccountExplicitly(mAccount, webViewPassword, null); + } + } + + // include account version with the new account + mAccountMgr.setUserData(mAccount, Constants.KEY_OC_ACCOUNT_VERSION, + Integer.toString(AccountUtils.ACCOUNT_VERSION)); + + /// add the new account as default in preferences, if there is none already + Account defaultAccount = AccountUtils.getCurrentOwnCloudAccount(this); + if (defaultAccount == null) { + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); + editor.putString("select_oc_account", accountName); + editor.apply(); + } + + /// prepare result to return to the Authenticator + // TODO check again what the Authenticator makes with it; probably has the same + // effect as addAccountExplicitly, but it's not well done + final Intent intent = new Intent(); + intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, accountType); + intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, mAccount.name); + intent.putExtra(AccountManager.KEY_USERDATA, username); + if (isOAuth || isSaml) { + mAccountMgr.setAuthToken(mAccount, mAuthTokenType, mAuthToken); + } + /// add user data to the new account; TODO probably can be done in the last parameter + // addAccountExplicitly, or in KEY_USERDATA + mAccountMgr.setUserData(mAccount, Constants.KEY_OC_VERSION, mServerInfo.mVersion.getVersion()); + mAccountMgr.setUserData(mAccount, Constants.KEY_OC_BASE_URL, mServerInfo.mBaseUrl); + + if (authResult.getData() != null) { + try { + UserInfo userInfo = (UserInfo) authResult.getData().get(0); + mAccountMgr.setUserData(mAccount, Constants.KEY_DISPLAY_NAME, userInfo.getDisplayName()); + mAccountMgr.setUserData(mAccount, Constants.KEY_USER_ID, userInfo.getId()); + } catch (ClassCastException c) { + Log_OC.w(TAG, "Couldn't get display name for " + username); + } + } else { + Log_OC.w(TAG, "Couldn't get display name for " + username); + } + + if (isSaml) { + mAccountMgr.setUserData(mAccount, Constants.KEY_SUPPORTS_SAML_WEB_SSO, "TRUE"); + } else if (isOAuth) { + mAccountMgr.setUserData(mAccount, Constants.KEY_SUPPORTS_OAUTH2, "TRUE"); + } + + setAccountAuthenticatorResult(intent.getExtras()); + setResult(RESULT_OK, intent); + + return true; + } + } + + /** + * Updates the content and visibility state of the icon and text associated + * to the last check on the ownCloud server. + */ + private void showServerStatus() { + if (mServerStatusIcon == 0 && "".equals(mServerStatusText)) { + mServerStatusView.setVisibility(View.INVISIBLE); + } else { + mServerStatusView.setText(mServerStatusText); + mServerStatusView.setCompoundDrawablesWithIntrinsicBounds(mServerStatusIcon, 0, 0, 0); + mServerStatusView.setVisibility(View.VISIBLE); + } + } + + + /** + * Updates the content and visibility state of the icon and text associated + * to the interactions with the OAuth authorization server. + */ + private void showAuthStatus() { + if (mAuthStatusIcon == 0 && "".equals(mAuthStatusText)) { + mAuthStatusView.setVisibility(View.INVISIBLE); + } else { + mAuthStatusView.setText(mAuthStatusText); + mAuthStatusView.setCompoundDrawablesWithIntrinsicBounds(mAuthStatusIcon, 0, 0, 0); + mAuthStatusView.setVisibility(View.VISIBLE); + } + } + + + private void showRefreshButton(boolean show) { + if (webViewLoginMethod && mRefreshButton != null) { + if (show) { + mRefreshButton.setVisibility(View.VISIBLE); + } else { + mRefreshButton.setVisibility(View.GONE); + } + } + } + + /** + * Called when the eye icon in the password field is clicked. + * + * Toggles the visibility of the password in the field. + */ + public void onViewPasswordClick() { + int selectionStart = mPasswordInput.getSelectionStart(); + int selectionEnd = mPasswordInput.getSelectionEnd(); + if (isPasswordVisible()) { + hidePassword(); + } else { + showPassword(); + } + mPasswordInput.setSelection(selectionStart, selectionEnd); + } + + + /** + * Called when the checkbox for OAuth authorization is clicked. + * + * Hides or shows the input fields for user & password. + * + * @param view 'View password' 'button' + */ + public void onCheckClick(View view) { + CheckBox oAuth2Check = (CheckBox) view; + if (oAuth2Check.isChecked()) { + mAuthTokenType = oauthTokenType; + } else { + mAuthTokenType = basicTokenType; + } + updateAuthenticationPreFragmentVisibility(); + } + + + /** + * Called when the 'action' button in an IME is pressed ('enter' in software keyboard). + * + * Used to trigger the authentication check when the user presses 'enter' after writing the + * password, or to throw the server test when the only field on screen is the URL input field. + */ + @Override + public boolean onEditorAction(TextView inputField, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE && inputField != null && + inputField.equals(mPasswordInput)) { + if (mOkButton.isEnabled()) { + mOkButton.performClick(); + } + + } else if ((actionId == EditorInfo.IME_ACTION_NEXT || actionId == EditorInfo.IME_NULL) + && inputField != null && inputField.equals(mHostUrlInput)) { + checkOcServer(); + } + return false; // always return false to grant that the software keyboard is hidden anyway + } + + + private abstract static class RightDrawableOnTouchListener implements OnTouchListener { + + private int fuzz = 75; + + /** + * {@inheritDoc} + */ + @Override + public boolean onTouch(View view, MotionEvent event) { + Drawable rightDrawable = null; + if (view instanceof TextView) { + Drawable[] drawables = ((TextView) view).getCompoundDrawables(); + if (drawables.length > 2) { + rightDrawable = drawables[2]; + } + } + if (rightDrawable != null) { + final int x = (int) event.getX(); + final int y = (int) event.getY(); + final Rect bounds = rightDrawable.getBounds(); + if (x >= (view.getRight() - bounds.width() - fuzz) && + x <= (view.getRight() - view.getPaddingRight() + fuzz) && + y >= (view.getPaddingTop() - fuzz) && + y <= (view.getHeight() - view.getPaddingBottom()) + fuzz) { + + return onDrawableTouch(event); + } + } + return false; + } + + public abstract boolean onDrawableTouch(final MotionEvent event); + } + + + private void getRemoteUserNameOperation(String sessionCookie) { + + Intent getUserNameIntent = new Intent(); + getUserNameIntent.setAction(OperationsService.ACTION_GET_USER_NAME); + getUserNameIntent.putExtra(OperationsService.EXTRA_SERVER_URL, mServerInfo.mBaseUrl); + getUserNameIntent.putExtra(OperationsService.EXTRA_COOKIE, sessionCookie); + + if (mOperationsServiceBinder != null) { + mWaitingForOpId = mOperationsServiceBinder.queueNewOperation(getUserNameIntent); + } + } + + + @Override + public void onSsoFinished(String sessionCookie) { + if (sessionCookie != null && sessionCookie.length() > 0) { + Log_OC.d(TAG, "Successful SSO - time to save the account"); + mAuthToken = sessionCookie; + getRemoteUserNameOperation(sessionCookie); + Fragment fd = getSupportFragmentManager().findFragmentByTag(SAML_DIALOG_TAG); + if (fd instanceof DialogFragment) { + Dialog d = ((DialogFragment) fd).getDialog(); + if (d != null && d.isShowing()) { + d.dismiss(); + } + } + + } else { + // TODO - show fail + Log_OC.d(TAG, "SSO failed"); + } + + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (AccountTypeUtils.getAuthTokenTypeSamlSessionCookie(MainApp.getAccountType(this)).equals(mAuthTokenType) && + mHostUrlInput.hasFocus() && event.getAction() == MotionEvent.ACTION_DOWN) { + checkOcServer(); + } + return super.onTouchEvent(event); + } + + + /** + * Show untrusted cert dialog + */ + public void showUntrustedCertDialog(X509Certificate x509Certificate, SslError error, SslErrorHandler handler) { + // Show a dialog with the certificate info + SslUntrustedCertDialog dialog; + if (x509Certificate == null) { + dialog = SslUntrustedCertDialog.newInstanceForEmptySslError(error, handler); + } else { + dialog = SslUntrustedCertDialog.newInstanceForFullSslError(x509Certificate, error, handler); + } + FragmentManager fm = getSupportFragmentManager(); + FragmentTransaction ft = fm.beginTransaction(); + ft.addToBackStack(null); + dialog.show(ft, UNTRUSTED_CERT_DIALOG_TAG); + } + + + /** + * Show untrusted cert dialog + */ + private void showUntrustedCertDialog(RemoteOperationResult result) { + // Show a dialog with the certificate info + SslUntrustedCertDialog dialog = SslUntrustedCertDialog. + newInstanceForFullSslError((CertificateCombinedException) result.getException()); + FragmentManager fm = getSupportFragmentManager(); + FragmentTransaction ft = fm.beginTransaction(); + ft.addToBackStack(null); + dialog.show(ft, UNTRUSTED_CERT_DIALOG_TAG); + + } + + /** + * Called from SslValidatorDialog when a new server certificate was correctly saved. + */ + public void onSavedCertificate() { + Fragment fd = getSupportFragmentManager().findFragmentByTag(SAML_DIALOG_TAG); + if (fd == null) { + // if SAML dialog is not shown, + // the SslDialog was shown due to an SSL error in the server check + checkOcServer(); + } + } + + /** + * Called from SslValidatorDialog when a new server certificate could not be saved + * when the user requested it. + */ + @Override + public void onFailedSavingCertificate() { + dismissDialog(SAML_DIALOG_TAG); + DisplayUtils.showSnackMessage(this, R.string.ssl_validator_not_saved); + } + + @Override + public void onCancelCertificate() { + dismissDialog(SAML_DIALOG_TAG); + } + + + private void doOnResumeAndBound() { + //Log_OC.e(TAG, "registering to listen for operation callbacks" ); + mOperationsServiceBinder.addOperationListener(AuthenticatorActivity.this, mHandler); + if (mWaitingForOpId <= Integer.MAX_VALUE) { + mOperationsServiceBinder.dispatchResultIfFinished((int) mWaitingForOpId, this); + } + + if (!webViewLoginMethod && mHostUrlInput.getText() != null && mHostUrlInput.getText().length() > 0 + && !mServerIsChecked) { + checkOcServer(); + } + } + + + private void dismissDialog(String dialogTag) { + Fragment frag = getSupportFragmentManager().findFragmentByTag(dialogTag); + if (frag instanceof DialogFragment) { + DialogFragment dialog = (DialogFragment) frag; + + try { + dialog.dismiss(); + } catch (IllegalStateException e) { + Log_OC.e(TAG, e.getMessage()); + dialog.dismissAllowingStateLoss(); + } + } + } + + + /** + * Implements callback methods for service binding. + */ + private class OperationsServiceConnection implements ServiceConnection { + + @Override + public void onServiceConnected(ComponentName component, IBinder service) { + if (component.equals( + new ComponentName(AuthenticatorActivity.this, OperationsService.class) + )) { + mOperationsServiceBinder = (OperationsServiceBinder) service; + + doOnResumeAndBound(); + + } + + } + + @Override + public void onServiceDisconnected(ComponentName component) { + if (component.equals( + new ComponentName(AuthenticatorActivity.this, OperationsService.class) + )) { + Log_OC.e(TAG, "Operations service crashed"); + mOperationsServiceBinder = null; + } + } + } + + /** + * Create and show dialog for request authentication to the user + * + * @param webView Web view to embed into the authentication dialog. + * @param handler Object responsible for catching and recovering HTTP authentication fails. + */ + public void createAuthenticationDialog(WebView webView, HttpAuthHandler handler) { + + // Show a dialog with the certificate info + CredentialsDialogFragment dialog = CredentialsDialogFragment.newInstanceForCredentials(webView, handler); + FragmentManager fm = getSupportFragmentManager(); + FragmentTransaction ft = fm.beginTransaction(); + ft.addToBackStack(null); + dialog.setCancelable(false); + dialog.show(ft, CREDENTIALS_DIALOG_TAG); + + if (!mIsFirstAuthAttempt) { + DisplayUtils.showSnackMessage(this, R.string.saml_authentication_wrong_pass); + } else { + mIsFirstAuthAttempt = false; + } + } + + /** + * For retrieving the clicking on authentication cancel button. + */ + public void doNegativeAuthenticationDialogClick() { + mIsFirstAuthAttempt = true; + } +} diff --git a/src/main/java/com/owncloud/android/features/FeatureItem.java b/src/main/java/com/owncloud/android/features/FeatureItem.java new file mode 100644 index 0000000000..e22ce9ca08 --- /dev/null +++ b/src/main/java/com/owncloud/android/features/FeatureItem.java @@ -0,0 +1,115 @@ +/* + * Nextcloud Android client application + * + * @author Bartosz Przybylski + * Copyright (C) 2015 Bartosz Przybylski + * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2016 Nextcloud. + * + * This program 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 program 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 program. If not, see . + */ + +package com.owncloud.android.features; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.owncloud.android.R; + +/** + * @author Bartosz Przybylski + * @author Tobias Kaminsky + */ +public class FeatureItem implements Parcelable { + private static final int DO_NOT_SHOW = -1; + private int image; + private int titleText; + private int contentText; + private boolean contentCentered; + private boolean bulletList; + + public FeatureItem(int image, int titleText, int contentText, boolean contentCentered, boolean bulletList) { + this.image = image; + this.titleText = titleText; + this.contentText = contentText; + this.contentCentered = contentCentered; + this.bulletList = bulletList; + } + + public boolean shouldShowImage() { + return image != DO_NOT_SHOW; + } + + public int getImage() { + return image; + } + + public boolean shouldShowTitleText() { + return titleText != DO_NOT_SHOW && titleText != R.string.empty; + } + + public int getTitleText() { + return titleText; + } + + public boolean shouldShowContentText() { + return contentText != DO_NOT_SHOW && contentText != R.string.empty; + } + + public int getContentText() { + return contentText; + } + + public boolean shouldContentCentered() { + return contentCentered; + } + + public boolean shouldShowBulletPointList() { + return bulletList; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(image); + dest.writeInt(titleText); + dest.writeInt(contentText); + dest.writeByte((byte) (contentCentered ? 1 : 0)); + dest.writeByte((byte) (bulletList ? 1 : 0)); + } + + private FeatureItem(Parcel p) { + image = p.readInt(); + titleText = p.readInt(); + contentText = p.readInt(); + contentCentered = p.readByte() == 1; + bulletList = p.readByte() == 1; + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public Object createFromParcel(Parcel source) { + return new FeatureItem(source); + } + + @Override + public Object[] newArray(int size) { + return new FeatureItem[size]; + } + }; +} diff --git a/src/main/java/com/owncloud/android/features/FeatureList.java b/src/main/java/com/owncloud/android/features/FeatureList.java deleted file mode 100644 index 8c8ac7344b..0000000000 --- a/src/main/java/com/owncloud/android/features/FeatureList.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Bartosz Przybylski - * Copyright (C) 2015 Bartosz Przybylski - * Copyright (C) 2015 ownCloud Inc. - * Copyright (C) 2016 Nextcloud. - * - * This program 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 program 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 program. If not, see . - */ - -package com.owncloud.android.features; - -import android.os.Parcel; -import android.os.Parcelable; - -import com.owncloud.android.MainApp; -import com.owncloud.android.R; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -/** - * @author Bartosz Przybylski - */ -public class FeatureList { - private static final boolean SHOW_ON_FIRST_RUN = true; - private static final boolean SHOW_ON_UPGRADE = false; - - private static final int VERSION_1_0_0 = 10000099; - private static final int VERSION_3_0_0 = 30000099; - private static final int VERSION_3_3_0 = 30030099; - private static final int BETA_VERSION_0 = 0; - - static public List get(boolean isMultiAccount) { - List featuresList = new ArrayList<>(); - // Basic features showed on first install - featuresList.add(new FeatureItem(R.drawable.whats_new_files, - R.string.welcome_feature_1_title, R.string.welcome_feature_1_text, - VERSION_1_0_0, BETA_VERSION_0, SHOW_ON_FIRST_RUN, true, false)); - if (isMultiAccount) { - featuresList.add(new FeatureItem(R.drawable.whats_new_accounts, - R.string.welcome_feature_2_title, R.string.welcome_feature_2_text, - VERSION_1_0_0, BETA_VERSION_0, SHOW_ON_FIRST_RUN, true, false)); - } - featuresList.add(new FeatureItem(R.drawable.whats_new_auto_upload, - R.string.welcome_feature_3_title, R.string.welcome_feature_3_text, - VERSION_1_0_0, BETA_VERSION_0, SHOW_ON_FIRST_RUN, true, false)); - - // 3.0.0 - featuresList.add(new FeatureItem(R.drawable.whats_new_end_to_end_encryption, - R.string.whats_new_end_to_end_encryption_title, R.string.whats_new_end_to_end_encryption_content, - VERSION_3_0_0, BETA_VERSION_0, SHOW_ON_UPGRADE, false, false)); - featuresList.add(new FeatureItem(R.drawable.whats_new_resized_images, R.string.whats_new_resized_images_title, - R.string.whats_new_resized_images_content, VERSION_3_0_0, BETA_VERSION_0, SHOW_ON_UPGRADE, - false, false)); - featuresList.add(new FeatureItem(R.drawable.whats_new_ipv6, R.string.whats_new_ipv6_title, - R.string.whats_new_ipv6_content, VERSION_3_0_0, - BETA_VERSION_0, SHOW_ON_UPGRADE, false, false)); - - // 3.3.0 - featuresList.add(new FeatureItem(R.drawable.whats_new_device_credentials, - R.string.whats_new_device_credentials_title, R.string.whats_new_device_credentials_content, - VERSION_3_3_0, BETA_VERSION_0, SHOW_ON_UPGRADE, false, false)); - - return featuresList; - } - - static public FeatureItem[] getFiltered(int lastSeenVersionCode, boolean isFirstRun, boolean isBeta, - boolean isMultiAccount) { - List features = new LinkedList<>(); - - for (FeatureItem item : get(isMultiAccount)) { - final int itemVersionCode = isBeta ? item.getBetaVersionNumber() : item.getVersionCode(); - if (isFirstRun && item.shouldShowOnFirstRun()) { - features.add(item); - } else if (!isFirstRun && !item.shouldShowOnFirstRun() && - MainApp.getVersionCode() >= itemVersionCode && - lastSeenVersionCode < itemVersionCode) { - features.add(item); - } - } - return features.toArray(new FeatureItem[features.size()]); - } - - static public class FeatureItem implements Parcelable { - public static final int DO_NOT_SHOW = -1; - private int image; - private int titleText; - private int contentText; - private int versionCode; - private int betaVersion; - private boolean showOnInitialRun; - private boolean contentCentered; - private boolean bulletList; - - public FeatureItem(int image, int titleText, int contentText, int version, int betaVersion) { - this(image, titleText, contentText, version, betaVersion, false, true, true); - } - - public FeatureItem(int image, int titleText, int contentText, int version, int betaVersion, - boolean showOnInitialRun) { - this(image, titleText, contentText, version, betaVersion, showOnInitialRun, true, true); - } - - public FeatureItem(int image, int titleText, int contentText, int versionCode, int betaVersion, - boolean showOnInitialRun, boolean contentCentered, boolean bulletList) { - this.image = image; - this.titleText = titleText; - this.contentText = contentText; - this.versionCode = versionCode; - this.betaVersion = betaVersion; - this.showOnInitialRun = showOnInitialRun; - this.contentCentered = contentCentered; - this.bulletList = bulletList; - } - - public boolean shouldShowImage() { return image != DO_NOT_SHOW; } - public int getImage() { return image; } - - public boolean shouldShowTitleText() { return titleText != DO_NOT_SHOW; } - public int getTitleText() { return titleText; } - - public boolean shouldShowContentText() { return contentText != DO_NOT_SHOW; } - public int getContentText() { return contentText; } - - public int getVersionCode() { - return versionCode; - } - public int getBetaVersionNumber() { return betaVersion; } - public boolean shouldShowOnFirstRun() { return showOnInitialRun; } - - public boolean shouldContentCentered() { - return contentCentered; - } - - public boolean shouldShowBulletPointList() { - return bulletList; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(image); - dest.writeInt(titleText); - dest.writeInt(contentText); - dest.writeInt(versionCode); - dest.writeInt(betaVersion); - dest.writeByte((byte) (showOnInitialRun ? 1 : 0)); - dest.writeByte((byte) (contentCentered ? 1 : 0)); - dest.writeByte((byte) (bulletList ? 1 : 0)); - } - - private FeatureItem(Parcel p) { - image = p.readInt(); - titleText = p.readInt(); - contentText = p.readInt(); - versionCode = p.readInt(); - betaVersion = p.readInt(); - showOnInitialRun = p.readByte() == 1; - contentCentered = p.readByte() == 1; - bulletList = p.readByte() == 1; - } - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - - @Override - public Object createFromParcel(Parcel source) { - return new FeatureItem(source); - } - - @Override - public Object[] newArray(int size) { - return new FeatureItem[size]; - } - }; - } -} diff --git a/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java b/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java index f772d139d9..6375ca6f22 100644 --- a/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java @@ -105,7 +105,7 @@ public abstract class BaseActivity extends AppCompatActivity { /** * Tries to swap the current ownCloud {@link Account} for other valid and existing. * - * If no valid ownCloud {@link Account} exists, the the user is requested + * If no valid ownCloud {@link Account} exists, then the user is requested * to create a new ownCloud {@link Account}. * * POSTCONDITION: updates {@link #mAccountWasSet} and {@link #mAccountWasRestored}. diff --git a/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java index 932f63e3af..31292a1d14 100644 --- a/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java @@ -446,7 +446,9 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU UserInfoActivity.openAccountRemovalConfirmationDialog(getAccount(), getFragmentManager(), true); break; case R.id.drawer_menu_account_add: - createAccount(false); + Intent firstRunIntent = new Intent(getApplicationContext(), FirstRunActivity.class); + firstRunIntent.putExtra(FirstRunActivity.EXTRA_ALLOW_CLOSE, true); + startActivity(firstRunIntent); break; case R.id.drawer_menu_account_manage: Intent manageAccountsIntent = new Intent(getApplicationContext(), ManageAccountsActivity.class); diff --git a/src/main/java/com/owncloud/android/ui/activity/FirstRunActivity.java b/src/main/java/com/owncloud/android/ui/activity/FirstRunActivity.java new file mode 100644 index 0000000000..b83a3d1480 --- /dev/null +++ b/src/main/java/com/owncloud/android/ui/activity/FirstRunActivity.java @@ -0,0 +1,228 @@ +/* + * Nextcloud Android client application + * + * @author Bartosz Przybylski + * Copyright (C) 2015 Bartosz Przybylski + * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2016 Nextcloud. + * + * This program 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 program 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 program. If not, see . + */ + +package com.owncloud.android.ui.activity; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v4.view.ViewPager; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.owncloud.android.MainApp; +import com.owncloud.android.R; +import com.owncloud.android.authentication.AccountUtils; +import com.owncloud.android.authentication.AuthenticatorActivity; +import com.owncloud.android.features.FeatureItem; +import com.owncloud.android.ui.adapter.FeaturesViewAdapter; +import com.owncloud.android.ui.whatsnew.ProgressIndicator; +import com.owncloud.android.utils.DisplayUtils; + +/** + * Activity displaying general feature after a fresh install. + */ +public class FirstRunActivity extends BaseActivity implements ViewPager.OnPageChangeListener { + + public static final String KEY_LAST_SEEN_VERSION_CODE = "lastSeenVersionCode"; + public static final String EXTRA_ALLOW_CLOSE = "ALLOW_CLOSE"; + public static final int FIRST_RUN_RESULT_CODE = 199; + + private static final String TAG = FirstRunActivity.class.getSimpleName(); + + private ProgressIndicator mProgress; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.first_run_activity); + + boolean isProviderOrOwnInstallationVisible = getResources().getBoolean(R.bool.show_provider_or_own_installation); + + setSlideshowSize(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); + + Button loginButton = findViewById(R.id.login); + loginButton.setBackgroundColor(Color.WHITE); + loginButton.setTextColor(Color.BLACK); + + loginButton.setOnClickListener(v -> { + if (getIntent().getBooleanExtra(EXTRA_ALLOW_CLOSE, false)) { + Intent authenticatorActivityIntent = new Intent(this, AuthenticatorActivity.class); + authenticatorActivityIntent.putExtra(AuthenticatorActivity.EXTRA_USE_PROVIDER_AS_WEBLOGIN, false); + startActivityForResult(authenticatorActivityIntent, FIRST_RUN_RESULT_CODE); + } else { + finish(); + } + }); + + Button providerButton = findViewById(R.id.signup); + providerButton.setBackgroundColor(getResources().getColor(R.color.primary_dark)); + providerButton.setTextColor(getResources().getColor(R.color.login_text_color)); + providerButton.setVisibility(isProviderOrOwnInstallationVisible ? View.VISIBLE : View.GONE); + providerButton.setOnClickListener(v -> { + Intent authenticatorActivityIntent = new Intent(this, AuthenticatorActivity.class); + authenticatorActivityIntent.putExtra(AuthenticatorActivity.EXTRA_USE_PROVIDER_AS_WEBLOGIN, true); + + if (getIntent().getBooleanExtra(EXTRA_ALLOW_CLOSE, false)) { + startActivityForResult(authenticatorActivityIntent, FIRST_RUN_RESULT_CODE); + } else { + authenticatorActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(authenticatorActivityIntent); + } + }); + + TextView hostOwnServerTextView = findViewById(R.id.host_own_server); + hostOwnServerTextView.setTextColor(getResources().getColor(R.color.login_text_color)); + hostOwnServerTextView.setVisibility(isProviderOrOwnInstallationVisible ? View.VISIBLE : View.GONE); + + mProgress = findViewById(R.id.progressIndicator); + ViewPager mPager = findViewById(R.id.contentPanel); + + // Sometimes, accounts are not deleted when you uninstall the application so we'll do it now + if (isFirstRun(this)) { + AccountManager am = (AccountManager) getSystemService(ACCOUNT_SERVICE); + if (am != null) { + for (Account account : AccountUtils.getAccounts(this)) { + am.removeAccount(account, null, null); + } + } + } + + FeaturesViewAdapter featuresViewAdapter = new FeaturesViewAdapter(getSupportFragmentManager(), + getFirstRun()); + mProgress.setNumberOfSteps(featuresViewAdapter.getCount()); + mPager.setAdapter(featuresViewAdapter); + + mPager.addOnPageChangeListener(this); + } + + private void setSlideshowSize(boolean isLandscape) { + boolean isProviderOrOwnInstallationVisible = getResources().getBoolean(R.bool.show_provider_or_own_installation); + LinearLayout buttonLayout = findViewById(R.id.buttonLayout); + LinearLayout.LayoutParams layoutParams; + + buttonLayout.setOrientation(isLandscape ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); + + LinearLayout bottomLayout = findViewById(R.id.bottomLayout); + if (isProviderOrOwnInstallationVisible) { + layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } else { + layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + DisplayUtils.convertDpToPixel(isLandscape ? 100f : 150f, this)); + } + + bottomLayout.setLayoutParams(layoutParams); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { + setSlideshowSize(true); + } else { + setSlideshowSize(false); + } + } + + @Override + public void onBackPressed() { + onFinish(); + + if (getIntent().getBooleanExtra(EXTRA_ALLOW_CLOSE, false)) { + super.onBackPressed(); + } + } + + private void onFinish() { + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = pref.edit(); + editor.putInt(KEY_LAST_SEEN_VERSION_CODE, MainApp.getVersionCode()); + editor.apply(); + } + + static private boolean isFirstRun(Context context) { + return AccountUtils.getCurrentOwnCloudAccount(context) == null; + } + + static public boolean runIfNeeded(Context context) { + if (context instanceof FirstRunActivity) { + return false; + } + + if (isFirstRun(context) && context instanceof AuthenticatorActivity) { + context.startActivity(new Intent(context, FirstRunActivity.class)); + return true; + } else { + return false; + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // unused but to be implemented due to abstract parent + } + + @Override + public void onPageSelected(int position) { + mProgress.animateToStep(position + 1); + } + + @Override + public void onPageScrollStateChanged(int state) { + // unused but to be implemented due to abstract parent + } + + public void onHostYourOwnServerClick(View view) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_server_install)))); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (FIRST_RUN_RESULT_CODE == requestCode && RESULT_OK == resultCode) { + setAccount(AccountUtils.getCurrentOwnCloudAccount(this)); + onAccountSet(false); + + Intent i = new Intent(this, FileDisplayActivity.class); + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(i); + } + } + + public static FeatureItem[] getFirstRun() { + return new FeatureItem[]{ + new FeatureItem(R.drawable.logo, R.string.first_run_1_text, R.string.empty, true, false), + new FeatureItem(R.drawable.first_run_files, R.string.first_run_2_text, R.string.empty, true, false), + new FeatureItem(R.drawable.first_run_groupware, R.string.first_run_3_text, R.string.empty, true, false), + new FeatureItem(R.drawable.first_run_talk, R.string.first_run_4_text, R.string.empty, true, false)}; + } +} diff --git a/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java b/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java index edea936ea5..eced4a35ea 100644 --- a/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java @@ -270,6 +270,13 @@ public class ManageAccountsActivity extends FileActivity } @Override + public void showFirstRunActivity() { + Intent firstRunIntent = new Intent(getApplicationContext(), FirstRunActivity.class); + firstRunIntent.putExtra(FirstRunActivity.EXTRA_ALLOW_CLOSE, true); + startActivity(firstRunIntent); + } + + // @Override public void createAccount() { AccountManager am = AccountManager.get(getApplicationContext()); am.addAccount(MainApp.getAccountType(this), @@ -286,9 +293,9 @@ public class ManageAccountsActivity extends FileActivity String name = result.getString(AccountManager.KEY_ACCOUNT_NAME); AccountUtils.setCurrentOwnCloudAccount(getApplicationContext(), name); mAccountListAdapter = new AccountListAdapter( - ManageAccountsActivity.this, - getAccountListItems(), - mTintedCheck + ManageAccountsActivity.this, + getAccountListItems(), + mTintedCheck ); mListView.setAdapter(mAccountListAdapter); runOnUiThread(new Runnable() { diff --git a/src/main/java/com/owncloud/android/ui/activity/WhatsNewActivity.java b/src/main/java/com/owncloud/android/ui/activity/WhatsNewActivity.java index 2763e03e9a..aedde89dec 100644 --- a/src/main/java/com/owncloud/android/ui/activity/WhatsNewActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/WhatsNewActivity.java @@ -22,48 +22,30 @@ package com.owncloud.android.ui.activity; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; -import android.text.SpannableString; -import android.text.Spanned; -import android.text.style.BulletSpan; -import android.view.Gravity; -import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; -import android.webkit.WebView; -import android.webkit.WebViewClient; import android.widget.Button; import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; import com.owncloud.android.MainApp; import com.owncloud.android.R; -import com.owncloud.android.authentication.AccountAuthenticatorActivity; import com.owncloud.android.authentication.AccountUtils; -import com.owncloud.android.features.FeatureList; -import com.owncloud.android.features.FeatureList.FeatureItem; +import com.owncloud.android.features.FeatureItem; +import com.owncloud.android.ui.adapter.FeaturesViewAdapter; +import com.owncloud.android.ui.adapter.FeaturesWebViewAdapter; import com.owncloud.android.ui.whatsnew.ProgressIndicator; import com.owncloud.android.utils.ThemeUtils; /** - * Activity displaying general feature after a fresh install and new features after an update. + * Activity displaying new features after an update. */ public class WhatsNewActivity extends FragmentActivity implements ViewPager.OnPageChangeListener { @@ -79,22 +61,12 @@ public class WhatsNewActivity extends FragmentActivity implements ViewPager.OnPa super.onCreate(savedInstanceState); setContentView(R.layout.whats_new_activity); - int fontColor = ThemeUtils.fontColor(this); + int fontColor = getResources().getColor(R.color.login_text_color); mProgress = findViewById(R.id.progressIndicator); mPager = findViewById(R.id.contentPanel); - final boolean isBeta = getResources().getBoolean(R.bool.is_beta); - final boolean isMultiAccount = getResources().getBoolean(R.bool.multiaccount_support); String[] urls = getResources().getStringArray(R.array.whatsnew_urls); - // Sometimes, accounts are not deleted when you uninstall the application so we'll do it now - if (isFirstRun()) { - AccountManager am = (AccountManager) getSystemService(ACCOUNT_SERVICE); - for (Account account : AccountUtils.getAccounts(this)) { - am.removeAccount(account, null, null); - } - } - boolean showWebView = urls.length > 0; if (showWebView) { @@ -104,29 +76,25 @@ public class WhatsNewActivity extends FragmentActivity implements ViewPager.OnPa mPager.setAdapter(featuresWebViewAdapter); } else { FeaturesViewAdapter featuresViewAdapter = new FeaturesViewAdapter(getSupportFragmentManager(), - FeatureList.getFiltered(getLastSeenVersionCode(), isFirstRun(), isBeta, isMultiAccount)); + getWhatsNew(this)); mProgress.setNumberOfSteps(featuresViewAdapter.getCount()); mPager.setAdapter(featuresViewAdapter); } mPager.addOnPageChangeListener(this); - mForwardFinishButton = findViewById(R.id.forward); ThemeUtils.colorImageButton(mForwardFinishButton, fontColor); - - mForwardFinishButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (mProgress.hasNextStep()) { - mPager.setCurrentItem(mPager.getCurrentItem() + 1, true); - mProgress.animateToStep(mPager.getCurrentItem() + 1); - } else { - onFinish(); - finish(); - } - updateNextButtonIfNeeded(); + + mForwardFinishButton.setOnClickListener(view -> { + if (mProgress.hasNextStep()) { + mPager.setCurrentItem(mPager.getCurrentItem() + 1, true); + mProgress.animateToStep(mPager.getCurrentItem() + 1); + } else { + onFinish(); + finish(); } + updateNextButtonIfNeeded(); }); if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { @@ -137,20 +105,15 @@ public class WhatsNewActivity extends FragmentActivity implements ViewPager.OnPa mSkipButton = findViewById(R.id.skip); mSkipButton.setTextColor(fontColor); - mSkipButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - onFinish(); - finish(); - } + mSkipButton.setOnClickListener(view -> { + onFinish(); + finish(); }); TextView tv = findViewById(R.id.welcomeText); if (showWebView) { tv.setText(R.string.app_name); - } else if (isFirstRun()) { - tv.setText(R.string.empty); } else { tv.setText(String.format(getString(R.string.whats_new_title), MainApp.getVersionName())); } @@ -164,7 +127,6 @@ public class WhatsNewActivity extends FragmentActivity implements ViewPager.OnPa super.onBackPressed(); } - private void updateNextButtonIfNeeded() { if (!mProgress.hasNextStep()) { mForwardFinishButton.setImageResource(R.drawable.ic_done_white); @@ -182,15 +144,6 @@ public class WhatsNewActivity extends FragmentActivity implements ViewPager.OnPa editor.apply(); } - static public int getLastSeenVersionCode() { - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(MainApp.getAppContext()); - return pref.getInt(KEY_LAST_SEEN_VERSION_CODE, 0); - } - - static private boolean isFirstRun() { - return getLastSeenVersionCode() == 0 && AccountUtils.getCurrentOwnCloudAccount(MainApp.getAppContext()) == null; - } - static public void runIfNeeded(Context context) { if (!context.getResources().getBoolean(R.bool.show_whats_new)) { return; @@ -206,12 +159,7 @@ public class WhatsNewActivity extends FragmentActivity implements ViewPager.OnPa } static private boolean shouldShow(Context context) { - final boolean isBeta = context.getResources().getBoolean(R.bool.is_beta); - final boolean isMultiAccount = context.getResources().getBoolean(R.bool.multiaccount_support); - - return (isFirstRun() && context instanceof AccountAuthenticatorActivity) || (!(isFirstRun() && - (context instanceof FileDisplayActivity)) && !(context instanceof PassCodeActivity) && - (FeatureList.getFiltered(getLastSeenVersionCode(), isFirstRun(), isBeta, isMultiAccount).length > 0)); + return !(context instanceof PassCodeActivity) && (getWhatsNew(context).length > 0); } @Override @@ -230,172 +178,22 @@ public class WhatsNewActivity extends FragmentActivity implements ViewPager.OnPa // unused but to be implemented due to abstract parent } - private final class FeaturesWebViewAdapter extends FragmentPagerAdapter { - private String[] mWebUrls; - - public FeaturesWebViewAdapter(FragmentManager fm, String[] webUrls) { - super(fm); - mWebUrls = webUrls; - } - - @Override - public Fragment getItem(int position) { - return FeatureWebFragment.newInstance(mWebUrls[position]); - } - - @Override - public int getCount() { - return mWebUrls.length; - } + static private boolean isFirstRun(Context context) { + return AccountUtils.getCurrentOwnCloudAccount(context) == null; } - public static class FeatureWebFragment extends Fragment { - private String mWebUrl; + static private FeatureItem[] getWhatsNew(Context context) { + int itemVersionCode = 30030000; - static public FeatureWebFragment newInstance(String webUrl) { - FeatureWebFragment f = new FeatureWebFragment(); - Bundle args = new Bundle(); - args.putString("url", webUrl); - f.setArguments(args); - return f; - } + int lastSeenVersionCode = MainApp.getLastSeenVersionCode(context); - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mWebUrl = getArguments() != null ? getArguments().getString("url") : null; - } - - @SuppressLint("SetJavaScriptEnabled") - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.whats_new_webview_element, container, false); - - WebView webView = v.findViewById(R.id.whatsNewWebView); - webView.getSettings().setJavaScriptEnabled(true); - webView.getSettings().setDomStorageEnabled(true); - webView.getSettings().setAllowFileAccess(false); - webView.setWebViewClient(new WebViewClient()); - webView.loadUrl(mWebUrl); - - return v; - } - } - - private final class FeaturesViewAdapter extends FragmentPagerAdapter { - - private FeatureItem[] mFeatures; - - public FeaturesViewAdapter(FragmentManager fm, FeatureItem[] features) { - super(fm); - mFeatures = features; - } - - @Override - public Fragment getItem(int position) { - return FeatureFragment.newInstance(mFeatures[position]); - } - - @Override - public int getCount() { - return mFeatures.length; - } - } - - public static class FeatureFragment extends Fragment { - private FeatureItem mItem; - - static public FeatureFragment newInstance(FeatureItem item) { - FeatureFragment f = new FeatureFragment(); - Bundle args = new Bundle(); - args.putParcelable("feature", item); - f.setArguments(args); - return f; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mItem = getArguments() != null ? (FeatureItem) getArguments().getParcelable("feature") : null; - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.whats_new_element, container, false); - int fontColor = ThemeUtils.fontColor(getContext()); - - ImageView iv = v.findViewById(R.id.whatsNewImage); - if (mItem.shouldShowImage()) { - iv.setImageResource(mItem.getImage()); - } - - TextView titleTextView = v.findViewById(R.id.whatsNewTitle); - if (mItem.shouldShowTitleText()) { - titleTextView.setText(mItem.getTitleText()); - titleTextView.setTextColor(fontColor); - } - - if (mItem.shouldShowContentText()) { - LinearLayout linearLayout = v.findViewById(R.id.whatsNewTextLayout); - - - if (mItem.shouldShowBulletPointList()) { - String[] texts = getText(mItem.getContentText()).toString().split("\n"); - - for (String text : texts) { - TextView textView = generateTextView(text, getContext(), - mItem.shouldContentCentered(), fontColor, true); - - linearLayout.addView(textView); - } - } else { - TextView textView = generateTextView(getText(mItem.getContentText()).toString(), - getContext(), mItem.shouldContentCentered(), fontColor, false); - - linearLayout.addView(textView); - } - } - - return v; - } - } - - private static TextView generateTextView(String text, Context context, - boolean shouldContentCentered, int fontColor, - boolean showBulletPoints) { - int standardMargin = context.getResources().getDimensionPixelSize(R.dimen.standard_margin); - int doubleMargin = context.getResources() - .getDimensionPixelSize(R.dimen.standard_double_margin); - int zeroMargin = context.getResources().getDimensionPixelSize(R.dimen.zero); - - TextView textView = new TextView(context); - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - layoutParams.setMargins(doubleMargin, standardMargin, doubleMargin, zeroMargin); - textView.setTextAppearance(context, R.style.NextcloudTextAppearanceMedium); - textView.setLayoutParams(layoutParams); - - if (showBulletPoints) { - BulletSpan bulletSpan = new BulletSpan(standardMargin, fontColor); - SpannableString spannableString = new SpannableString(text); - spannableString.setSpan(bulletSpan, 0, spannableString.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - textView.setText(spannableString); + if (!isFirstRun(context) && MainApp.getVersionCode() >= itemVersionCode + && lastSeenVersionCode < itemVersionCode) { + return new FeatureItem[]{(new FeatureItem(R.drawable.whats_new_device_credentials, + R.string.whats_new_device_credentials_title, R.string.whats_new_device_credentials_content, + false, false))}; } else { - textView.setText(text); + return new FeatureItem[0]; } - textView.setTextColor(fontColor); - - if (!shouldContentCentered) { - textView.setGravity(Gravity.START); - } else { - textView.setGravity(Gravity.CENTER_HORIZONTAL); - } - - return textView; } } diff --git a/src/main/java/com/owncloud/android/ui/adapter/AccountListAdapter.java b/src/main/java/com/owncloud/android/ui/adapter/AccountListAdapter.java index 3e4810fe76..adad7b6ebb 100644 --- a/src/main/java/com/owncloud/android/ui/adapter/AccountListAdapter.java +++ b/src/main/java/com/owncloud/android/ui/adapter/AccountListAdapter.java @@ -127,7 +127,7 @@ public class AccountListAdapter extends ArrayAdapter implements ((ImageView) actionView.findViewById(R.id.user_icon)).setImageResource(R.drawable.ic_account_plus); // bind action listener - actionView.setOnClickListener(v -> mListener.createAccount()); + actionView.setOnClickListener(v -> mListener.showFirstRunActivity()); return actionView; } @@ -184,7 +184,7 @@ public class AccountListAdapter extends ArrayAdapter implements */ public interface AccountListAdapterListener { - void createAccount(); + void showFirstRunActivity(); } /** diff --git a/src/main/java/com/owncloud/android/ui/adapter/FeaturesViewAdapter.java b/src/main/java/com/owncloud/android/ui/adapter/FeaturesViewAdapter.java new file mode 100644 index 0000000000..beb392150d --- /dev/null +++ b/src/main/java/com/owncloud/android/ui/adapter/FeaturesViewAdapter.java @@ -0,0 +1,28 @@ +package com.owncloud.android.ui.adapter; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; + +import com.owncloud.android.features.FeatureItem; +import com.owncloud.android.ui.fragment.FeatureFragment; + +public class FeaturesViewAdapter extends FragmentPagerAdapter { + + private FeatureItem[] mFeatures; + + public FeaturesViewAdapter(FragmentManager fm, FeatureItem[] features) { + super(fm); + mFeatures = features; + } + + @Override + public Fragment getItem(int position) { + return FeatureFragment.newInstance(mFeatures[position]); + } + + @Override + public int getCount() { + return mFeatures.length; + } +} \ No newline at end of file diff --git a/src/main/java/com/owncloud/android/ui/adapter/FeaturesWebViewAdapter.java b/src/main/java/com/owncloud/android/ui/adapter/FeaturesWebViewAdapter.java new file mode 100644 index 0000000000..c7e2520be5 --- /dev/null +++ b/src/main/java/com/owncloud/android/ui/adapter/FeaturesWebViewAdapter.java @@ -0,0 +1,26 @@ +package com.owncloud.android.ui.adapter; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; + +import com.owncloud.android.ui.fragment.FeatureWebFragment; + +public class FeaturesWebViewAdapter extends FragmentPagerAdapter { + private String[] mWebUrls; + + public FeaturesWebViewAdapter(FragmentManager fm, String[] webUrls) { + super(fm); + mWebUrls = webUrls; + } + + @Override + public Fragment getItem(int position) { + return FeatureWebFragment.newInstance(mWebUrls[position]); + } + + @Override + public int getCount() { + return mWebUrls.length; + } +} \ No newline at end of file diff --git a/src/main/java/com/owncloud/android/ui/fragment/FeatureFragment.java b/src/main/java/com/owncloud/android/ui/fragment/FeatureFragment.java new file mode 100644 index 0000000000..e609f3085d --- /dev/null +++ b/src/main/java/com/owncloud/android/ui/fragment/FeatureFragment.java @@ -0,0 +1,119 @@ +package com.owncloud.android.ui.fragment; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.BulletSpan; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.owncloud.android.R; +import com.owncloud.android.features.FeatureItem; + + +public class FeatureFragment extends Fragment { + private FeatureItem mItem; + + static public FeatureFragment newInstance(FeatureItem item) { + FeatureFragment f = new FeatureFragment(); + Bundle args = new Bundle(); + args.putParcelable("feature", item); + f.setArguments(args); + return f; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mItem = getArguments() != null ? (FeatureItem) getArguments().getParcelable("feature") : null; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.whats_new_element, container, false); + int fontColor = getResources().getColor(R.color.login_text_color); + + ImageView iv = v.findViewById(R.id.whatsNewImage); + if (mItem.shouldShowImage()) { + iv.setImageResource(mItem.getImage()); + } + + TextView titleTextView = v.findViewById(R.id.whatsNewTitle); + if (mItem.shouldShowTitleText()) { + titleTextView.setText(mItem.getTitleText()); + titleTextView.setTextColor(fontColor); + titleTextView.setVisibility(View.VISIBLE); + } else { + titleTextView.setVisibility(View.GONE); + } + + LinearLayout linearLayout = v.findViewById(R.id.whatsNewTextLayout); + if (mItem.shouldShowContentText()) { + if (mItem.shouldShowBulletPointList()) { + String[] texts = getText(mItem.getContentText()).toString().split("\n"); + + for (String text : texts) { + TextView textView = generateTextView(text, getContext(), + mItem.shouldContentCentered(), fontColor, true); + + linearLayout.addView(textView); + } + } else { + TextView textView = generateTextView(getText(mItem.getContentText()).toString(), + getContext(), mItem.shouldContentCentered(), fontColor, false); + + linearLayout.addView(textView); + } + } else { + linearLayout.setVisibility(View.GONE); + } + + return v; + } + + private TextView generateTextView(String text, Context context, + boolean shouldContentCentered, int fontColor, + boolean showBulletPoints) { + int standardMargin = context.getResources().getDimensionPixelSize(R.dimen.standard_margin); + int doubleMargin = context.getResources() + .getDimensionPixelSize(R.dimen.standard_double_margin); + int zeroMargin = context.getResources().getDimensionPixelSize(R.dimen.zero); + + TextView textView = new TextView(context); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + layoutParams.setMargins(doubleMargin, standardMargin, doubleMargin, zeroMargin); + textView.setTextAppearance(context, R.style.NextcloudTextAppearanceMedium); + textView.setLayoutParams(layoutParams); + + if (showBulletPoints) { + BulletSpan bulletSpan = new BulletSpan(standardMargin, fontColor); + SpannableString spannableString = new SpannableString(text); + spannableString.setSpan(bulletSpan, 0, spannableString.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + textView.setText(spannableString); + } else { + textView.setText(text); + } + textView.setTextColor(fontColor); + + if (!shouldContentCentered) { + textView.setGravity(Gravity.START); + } else { + textView.setGravity(Gravity.CENTER_HORIZONTAL); + } + + return textView; + } +} \ No newline at end of file diff --git a/src/main/java/com/owncloud/android/ui/fragment/FeatureWebFragment.java b/src/main/java/com/owncloud/android/ui/fragment/FeatureWebFragment.java new file mode 100644 index 0000000000..59feea0105 --- /dev/null +++ b/src/main/java/com/owncloud/android/ui/fragment/FeatureWebFragment.java @@ -0,0 +1,49 @@ +package com.owncloud.android.ui.fragment; + + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import com.owncloud.android.R; + +public class FeatureWebFragment extends Fragment { + private String mWebUrl; + + static public FeatureWebFragment newInstance(String webUrl) { + FeatureWebFragment f = new FeatureWebFragment(); + Bundle args = new Bundle(); + args.putString("url", webUrl); + f.setArguments(args); + return f; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mWebUrl = getArguments() != null ? getArguments().getString("url") : null; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.whats_new_webview_element, container, false); + + WebView webView = v.findViewById(R.id.whatsNewWebView); + webView.getSettings().setJavaScriptEnabled(true); + webView.getSettings().setDomStorageEnabled(true); + webView.getSettings().setAllowFileAccess(false); + webView.setWebViewClient(new WebViewClient()); + webView.loadUrl(mWebUrl); + + return v; + } +} \ No newline at end of file diff --git a/src/main/java/com/owncloud/android/ui/whatsnew/ProgressIndicator.java b/src/main/java/com/owncloud/android/ui/whatsnew/ProgressIndicator.java index edf399a2ab..c117dc5b08 100644 --- a/src/main/java/com/owncloud/android/ui/whatsnew/ProgressIndicator.java +++ b/src/main/java/com/owncloud/android/ui/whatsnew/ProgressIndicator.java @@ -33,7 +33,6 @@ import android.widget.ImageView; import android.widget.LinearLayout; import com.owncloud.android.R; -import com.owncloud.android.utils.ThemeUtils; /** * Progress indicator visualizing the actual progress with dots. @@ -65,7 +64,7 @@ public class ProgressIndicator extends FrameLayout { } public void setNumberOfSteps(int steps) { - int fontColor = ThemeUtils.fontColor(getContext()); + int fontColor = getResources().getColor(R.color.login_text_color); mNumberOfSteps = steps; mDotsContainer.removeAllViews(); for (int i = 0; i < steps; ++i) { diff --git a/src/main/java/com/owncloud/android/utils/DisplayUtils.java b/src/main/java/com/owncloud/android/utils/DisplayUtils.java index a5aeb19bde..7edce285e5 100644 --- a/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -50,6 +50,7 @@ import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.style.StyleSpan; +import android.util.DisplayMetrics; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -762,6 +763,13 @@ public class DisplayUtils { } } + public static int convertDpToPixel(float dp, Context context) { + Resources resources = context.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + + return (int) (dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT)); + } + static public void showServerOutdatedSnackbar(Activity activity) { Snackbar.make(activity.findViewById(android.R.id.content), R.string.outdated_server, Snackbar.LENGTH_INDEFINITE) diff --git a/src/main/res/drawable/first_run_files.xml b/src/main/res/drawable/first_run_files.xml new file mode 100644 index 0000000000..3c134cf4ae --- /dev/null +++ b/src/main/res/drawable/first_run_files.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/src/main/res/drawable/first_run_groupware.xml b/src/main/res/drawable/first_run_groupware.xml new file mode 100644 index 0000000000..38d32c2389 --- /dev/null +++ b/src/main/res/drawable/first_run_groupware.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/first_run_talk.xml b/src/main/res/drawable/first_run_talk.xml new file mode 100644 index 0000000000..9958a72452 --- /dev/null +++ b/src/main/res/drawable/first_run_talk.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout-land/account_setup.xml b/src/main/res/layout-land/account_setup.xml index 55bd979557..06df2f249a 100644 --- a/src/main/res/layout-land/account_setup.xml +++ b/src/main/res/layout-land/account_setup.xml @@ -288,26 +288,6 @@ android:text="@string/setup_btn_connect" android:contentDescription="@string/setup_btn_connect" android:visibility="gone"/> - - - -