Loading app/build.gradle +3 −0 Original line number Diff line number Diff line Loading @@ -26,6 +26,9 @@ android { disable 'MissingTranslation' abortOnError false } dataBinding { enabled = true } } dependencies { Loading app/src/main/java/it/niedermann/owncloud/notes/android/activity/AboutActivity.java +6 −0 Original line number Diff line number Diff line Loading @@ -88,4 +88,10 @@ public class AboutActivity extends AppCompatActivity { } } } @Override public boolean onSupportNavigateUp() { finish(); // close this activity as oppose to navigating up return true; } } No newline at end of file app/src/main/java/it/niedermann/owncloud/notes/android/activity/SettingsActivity.java +278 −25 Original line number Diff line number Diff line Loading @@ -3,25 +3,46 @@ package it.niedermann.owncloud.notes.android.activity; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.net.http.SslCertificate; import android.net.http.SslError; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.webkit.SslErrorHandler; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Button; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputLayout; import java.io.ByteArrayInputStream; import java.net.URLDecoder; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Locale; import java.util.Map; import at.bitfire.cert4android.CustomCertManager; import at.bitfire.cert4android.IOnCertificateDecision; import butterknife.BindView; import butterknife.ButterKnife; import it.niedermann.owncloud.notes.R; Loading @@ -45,10 +66,15 @@ public class SettingsActivity extends AppCompatActivity { public static final String DEFAULT_SETTINGS = ""; public static final int CREDENTIALS_CHANGED = 3; public static final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"; public static final String WEBDAV_PATH_4_0_AND_LATER = "/remote.php/webdav"; private SharedPreferences preferences = null; @BindView(R.id.settings_url) EditText field_url; @BindView(R.id.settings_username_wrapper) TextInputLayout username_wrapper; @BindView(R.id.settings_username) EditText field_username; @BindView(R.id.settings_password) Loading @@ -61,7 +87,10 @@ public class SettingsActivity extends AppCompatActivity { View urlWarnHttp; private String old_password = ""; private WebView webView; private boolean first_run = false; private boolean useWebLogin = true; @Override public void onCreate(Bundle savedInstanceState) { Loading @@ -80,10 +109,38 @@ public class SettingsActivity extends AppCompatActivity { } } field_url.setOnFocusChangeListener((View v, boolean hasFocus) -> { new URLValidatorAsyncTask().execute(NotesClientUtil.formatURL(field_url.getText().toString())); setupListener(); // Load current Preferences field_url.setText(preferences.getString(SETTINGS_URL, DEFAULT_SETTINGS)); field_username.setText(preferences.getString(SETTINGS_USERNAME, DEFAULT_SETTINGS)); old_password = preferences.getString(SETTINGS_PASSWORD, DEFAULT_SETTINGS); field_password.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { login(); return true; } }); field_password.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { setPasswordHint(hasFocus); } }); setPasswordHint(false); handleSubmitButtonEnabled(); } private void setupListener() { field_url.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { new URLValidatorAsyncTask().execute(NotesClientUtil.formatURL(field_url.getText().toString())); } }); field_url.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { Loading @@ -99,7 +156,7 @@ public class SettingsActivity extends AppCompatActivity { urlWarnHttp.setVisibility(View.GONE); } handleSubmitButtonEnabled(field_url.getText(), field_username.getText()); handleSubmitButtonEnabled(); } @Override Loading @@ -115,7 +172,7 @@ public class SettingsActivity extends AppCompatActivity { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { handleSubmitButtonEnabled(field_url.getText(), field_username.getText()); handleSubmitButtonEnabled(); } @Override Loading @@ -124,23 +181,11 @@ public class SettingsActivity extends AppCompatActivity { } }); // Load current Preferences field_url.setText(preferences.getString(SETTINGS_URL, DEFAULT_SETTINGS)); field_username.setText(preferences.getString(SETTINGS_USERNAME, DEFAULT_SETTINGS)); old_password = preferences.getString(SETTINGS_PASSWORD, DEFAULT_SETTINGS); field_password.setOnEditorActionListener((TextView v, int actionId, KeyEvent event) -> { login(); return true; }); field_password.setOnFocusChangeListener((View v, boolean hasFocus) -> { setPasswordHint(hasFocus); }); setPasswordHint(false); btn_submit.setEnabled(false); btn_submit.setOnClickListener((View v) -> { btn_submit.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { login(); } }); } Loading Loading @@ -171,7 +216,7 @@ public class SettingsActivity extends AppCompatActivity { } } private void login() { private void legacyLogin() { String url = field_url.getText().toString().trim(); String username = field_username.getText().toString(); String password = field_password.getText().toString(); Loading @@ -185,8 +230,205 @@ public class SettingsActivity extends AppCompatActivity { new LoginValidatorAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, username, password); } private void handleSubmitButtonEnabled(Editable url, Editable username) { if (field_username.getText().length() > 0 && field_url.getText().length() > 0) { private void login() { if (useWebLogin) { webLogin(); } else { legacyLogin(); } } /** * Obtain the X509Certificate from SslError * * @param error SslError * @return X509Certificate from error */ public static X509Certificate getX509CertificateFromError(SslError error) { Bundle bundle = SslCertificate.saveState(error.getCertificate()); X509Certificate x509Certificate; byte[] bytes = bundle.getByteArray("x509-certificate"); if (bytes == null) { x509Certificate = null; } else { try { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes)); x509Certificate = (X509Certificate) cert; } catch (CertificateException e) { x509Certificate = null; } } return x509Certificate; } private void webLogin() { setContentView(R.layout.activity_settings_webview); webView = findViewById(R.id.login_webview); webView.setVisibility(View.GONE); final ProgressBar progressBar = findViewById(R.id.login_webview_progress_bar); WebSettings settings = webView.getSettings(); settings.setAllowFileAccess(false); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); settings.setUserAgentString(getWebLoginUserAgent()); settings.setSaveFormData(false); settings.setSavePassword(false); Map<String, String> headers = new HashMap<>(); headers.put("OCS-APIREQUEST", "true"); webView.loadUrl(normalizeUrlSuffix(NotesClientUtil.formatURL(field_url.getText().toString())) + "index.php/login/flow", headers); webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith("nc://login/")) { parseAndLoginFromWebView(url); return true; } return false; } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); progressBar.setVisibility(View.GONE); webView.setVisibility(View.VISIBLE); } @Override public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) { X509Certificate cert = getX509CertificateFromError(error); try { final boolean[] accepted = new boolean[1]; NoteServerSyncHelper.getInstance(NoteSQLiteOpenHelper.getInstance(getApplicationContext())) .checkCertificate(cert.getEncoded(), true, new IOnCertificateDecision.Stub() { @Override public void accept() { Log.d("Note", "cert accepted"); handler.proceed(); accepted[0] = true; } @Override public void reject() { Log.d("Note", "cert rejected"); handler.cancel(); } }); } catch (Exception e) { Log.e("Note", "Cert could not be verified"); handler.proceed(); } } }); // show snackbar after 60s to switch back to old login method new Handler().postDelayed(() -> { Snackbar.make(webView, R.string.fallback_weblogin_text, Snackbar.LENGTH_INDEFINITE) .setAction(R.string.fallback_weblogin_back, (View.OnClickListener) v -> initLegacyLogin(field_url.getText().toString())).show(); }, 45 * 1000); } private String getWebLoginUserAgent() { return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) + Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL; } private void parseAndLoginFromWebView(String dataString) { String prefix = "nc://login/"; LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, dataString); if (loginUrlInfo != null) { String url = normalizeUrlSuffix(loginUrlInfo.serverAddress); new LoginValidatorAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, loginUrlInfo.username, loginUrlInfo.password); } } /** * 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 */ private 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 String normalizeUrlSuffix(String url) { if (url.toLowerCase(Locale.ROOT).endsWith(WEBDAV_PATH_4_0_AND_LATER)) { return url.substring(0, url.length() - WEBDAV_PATH_4_0_AND_LATER.length()); } if (!url.endsWith("/")) { return url + "/"; } return url; } private void initLegacyLogin(String oldUrl) { useWebLogin = false; new URLValidatorAsyncTask().execute(NotesClientUtil.formatURL(field_url.getText().toString())); webView.setVisibility(View.INVISIBLE); setContentView(R.layout.activity_settings); ButterKnife.bind(this); setupListener(); field_url.setText(oldUrl); username_wrapper.setVisibility(View.VISIBLE); password_wrapper.setVisibility(View.VISIBLE); } private void handleSubmitButtonEnabled() { // drawable[2] is not null if url is valid, see URLValidatorAsyncTask::onPostExecute if (useWebLogin || field_url.getCompoundDrawables()[2] != null && (username_wrapper.getVisibility() == View.GONE || (username_wrapper.getVisibility() == View.VISIBLE && field_username.getText().length() > 0))) { btn_submit.setEnabled(true); } else { btn_submit.setEnabled(false); Loading @@ -203,6 +445,7 @@ public class SettingsActivity extends AppCompatActivity { @Override protected void onPreExecute() { btn_submit.setEnabled(false); field_url.setCompoundDrawables(null, null, null, null); } Loading @@ -221,6 +464,7 @@ public class SettingsActivity extends AppCompatActivity { } else { field_url.setCompoundDrawables(null, null, null, null); } handleSubmitButtonEnabled(); } } Loading Loading @@ -284,4 +528,13 @@ public class SettingsActivity extends AppCompatActivity { field_password.setEnabled(enabled); } } /** * Data object holding the login url fields. */ public class LoginUrlInfo { String serverAddress; String username; String password; } } app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java +10 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.IBinder; import android.os.RemoteException; import android.preference.PreferenceManager; import android.util.Log; import android.widget.Toast; Loading @@ -27,6 +28,8 @@ import java.util.Set; import at.bitfire.cert4android.CustomCertManager; import at.bitfire.cert4android.CustomCertService; import at.bitfire.cert4android.ICustomCertService; import at.bitfire.cert4android.IOnCertificateDecision; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.android.activity.SettingsActivity; import it.niedermann.owncloud.notes.model.CloudNote; Loading Loading @@ -64,6 +67,7 @@ public class NoteServerSyncHelper { private final Context appContext; private CustomCertManager customCertManager; private ICustomCertService iCustomCertService; // Track network connection changes using a BroadcastReceiver private boolean networkConnected = false; Loading Loading @@ -94,6 +98,7 @@ public class NoteServerSyncHelper { private final ServiceConnection certService = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { iCustomCertService = ICustomCertService.Stub.asInterface(iBinder); cert4androidReady = true; if (isSyncPossible()) { scheduleSync(false); Loading @@ -103,6 +108,7 @@ public class NoteServerSyncHelper { @Override public void onServiceDisconnected(ComponentName componentName) { cert4androidReady = false; iCustomCertService = null; } }; Loading Loading @@ -168,6 +174,10 @@ public class NoteServerSyncHelper { return customCertManager; } public void checkCertificate(byte[] cert, boolean foreground, IOnCertificateDecision callback) throws RemoteException { iCustomCertService.checkTrusted(cert, true, foreground, callback); } /** * Adds a callback method to the NoteServerSyncHelper for the synchronization part push local changes to the server. * All callbacks will be executed once the synchronization operations are done. Loading app/src/main/java/it/niedermann/owncloud/notes/util/SupportUtil.java +3 −2 Original line number Diff line number Diff line Loading @@ -4,13 +4,14 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; import androidx.annotation.WorkerThread; import android.text.Html; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.util.Log; import android.widget.TextView; import androidx.annotation.WorkerThread; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; Loading Loading @@ -93,6 +94,6 @@ public class SupportUtil { @WorkerThread public static CustomCertManager getCertManager(Context ctx) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); return new CustomCertManager(ctx, preferences.getBoolean(ctx.getString(R.string.pref_key_trust_system_certs), true)); return new CustomCertManager(ctx, preferences.getBoolean(ctx.getString(R.string.pref_key_trust_system_certs), true), true, true); } } Loading
app/build.gradle +3 −0 Original line number Diff line number Diff line Loading @@ -26,6 +26,9 @@ android { disable 'MissingTranslation' abortOnError false } dataBinding { enabled = true } } dependencies { Loading
app/src/main/java/it/niedermann/owncloud/notes/android/activity/AboutActivity.java +6 −0 Original line number Diff line number Diff line Loading @@ -88,4 +88,10 @@ public class AboutActivity extends AppCompatActivity { } } } @Override public boolean onSupportNavigateUp() { finish(); // close this activity as oppose to navigating up return true; } } No newline at end of file
app/src/main/java/it/niedermann/owncloud/notes/android/activity/SettingsActivity.java +278 −25 Original line number Diff line number Diff line Loading @@ -3,25 +3,46 @@ package it.niedermann.owncloud.notes.android.activity; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.net.http.SslCertificate; import android.net.http.SslError; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.webkit.SslErrorHandler; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Button; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputLayout; import java.io.ByteArrayInputStream; import java.net.URLDecoder; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Locale; import java.util.Map; import at.bitfire.cert4android.CustomCertManager; import at.bitfire.cert4android.IOnCertificateDecision; import butterknife.BindView; import butterknife.ButterKnife; import it.niedermann.owncloud.notes.R; Loading @@ -45,10 +66,15 @@ public class SettingsActivity extends AppCompatActivity { public static final String DEFAULT_SETTINGS = ""; public static final int CREDENTIALS_CHANGED = 3; public static final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"; public static final String WEBDAV_PATH_4_0_AND_LATER = "/remote.php/webdav"; private SharedPreferences preferences = null; @BindView(R.id.settings_url) EditText field_url; @BindView(R.id.settings_username_wrapper) TextInputLayout username_wrapper; @BindView(R.id.settings_username) EditText field_username; @BindView(R.id.settings_password) Loading @@ -61,7 +87,10 @@ public class SettingsActivity extends AppCompatActivity { View urlWarnHttp; private String old_password = ""; private WebView webView; private boolean first_run = false; private boolean useWebLogin = true; @Override public void onCreate(Bundle savedInstanceState) { Loading @@ -80,10 +109,38 @@ public class SettingsActivity extends AppCompatActivity { } } field_url.setOnFocusChangeListener((View v, boolean hasFocus) -> { new URLValidatorAsyncTask().execute(NotesClientUtil.formatURL(field_url.getText().toString())); setupListener(); // Load current Preferences field_url.setText(preferences.getString(SETTINGS_URL, DEFAULT_SETTINGS)); field_username.setText(preferences.getString(SETTINGS_USERNAME, DEFAULT_SETTINGS)); old_password = preferences.getString(SETTINGS_PASSWORD, DEFAULT_SETTINGS); field_password.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { login(); return true; } }); field_password.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { setPasswordHint(hasFocus); } }); setPasswordHint(false); handleSubmitButtonEnabled(); } private void setupListener() { field_url.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { new URLValidatorAsyncTask().execute(NotesClientUtil.formatURL(field_url.getText().toString())); } }); field_url.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { Loading @@ -99,7 +156,7 @@ public class SettingsActivity extends AppCompatActivity { urlWarnHttp.setVisibility(View.GONE); } handleSubmitButtonEnabled(field_url.getText(), field_username.getText()); handleSubmitButtonEnabled(); } @Override Loading @@ -115,7 +172,7 @@ public class SettingsActivity extends AppCompatActivity { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { handleSubmitButtonEnabled(field_url.getText(), field_username.getText()); handleSubmitButtonEnabled(); } @Override Loading @@ -124,23 +181,11 @@ public class SettingsActivity extends AppCompatActivity { } }); // Load current Preferences field_url.setText(preferences.getString(SETTINGS_URL, DEFAULT_SETTINGS)); field_username.setText(preferences.getString(SETTINGS_USERNAME, DEFAULT_SETTINGS)); old_password = preferences.getString(SETTINGS_PASSWORD, DEFAULT_SETTINGS); field_password.setOnEditorActionListener((TextView v, int actionId, KeyEvent event) -> { login(); return true; }); field_password.setOnFocusChangeListener((View v, boolean hasFocus) -> { setPasswordHint(hasFocus); }); setPasswordHint(false); btn_submit.setEnabled(false); btn_submit.setOnClickListener((View v) -> { btn_submit.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { login(); } }); } Loading Loading @@ -171,7 +216,7 @@ public class SettingsActivity extends AppCompatActivity { } } private void login() { private void legacyLogin() { String url = field_url.getText().toString().trim(); String username = field_username.getText().toString(); String password = field_password.getText().toString(); Loading @@ -185,8 +230,205 @@ public class SettingsActivity extends AppCompatActivity { new LoginValidatorAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, username, password); } private void handleSubmitButtonEnabled(Editable url, Editable username) { if (field_username.getText().length() > 0 && field_url.getText().length() > 0) { private void login() { if (useWebLogin) { webLogin(); } else { legacyLogin(); } } /** * Obtain the X509Certificate from SslError * * @param error SslError * @return X509Certificate from error */ public static X509Certificate getX509CertificateFromError(SslError error) { Bundle bundle = SslCertificate.saveState(error.getCertificate()); X509Certificate x509Certificate; byte[] bytes = bundle.getByteArray("x509-certificate"); if (bytes == null) { x509Certificate = null; } else { try { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes)); x509Certificate = (X509Certificate) cert; } catch (CertificateException e) { x509Certificate = null; } } return x509Certificate; } private void webLogin() { setContentView(R.layout.activity_settings_webview); webView = findViewById(R.id.login_webview); webView.setVisibility(View.GONE); final ProgressBar progressBar = findViewById(R.id.login_webview_progress_bar); WebSettings settings = webView.getSettings(); settings.setAllowFileAccess(false); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); settings.setUserAgentString(getWebLoginUserAgent()); settings.setSaveFormData(false); settings.setSavePassword(false); Map<String, String> headers = new HashMap<>(); headers.put("OCS-APIREQUEST", "true"); webView.loadUrl(normalizeUrlSuffix(NotesClientUtil.formatURL(field_url.getText().toString())) + "index.php/login/flow", headers); webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith("nc://login/")) { parseAndLoginFromWebView(url); return true; } return false; } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); progressBar.setVisibility(View.GONE); webView.setVisibility(View.VISIBLE); } @Override public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) { X509Certificate cert = getX509CertificateFromError(error); try { final boolean[] accepted = new boolean[1]; NoteServerSyncHelper.getInstance(NoteSQLiteOpenHelper.getInstance(getApplicationContext())) .checkCertificate(cert.getEncoded(), true, new IOnCertificateDecision.Stub() { @Override public void accept() { Log.d("Note", "cert accepted"); handler.proceed(); accepted[0] = true; } @Override public void reject() { Log.d("Note", "cert rejected"); handler.cancel(); } }); } catch (Exception e) { Log.e("Note", "Cert could not be verified"); handler.proceed(); } } }); // show snackbar after 60s to switch back to old login method new Handler().postDelayed(() -> { Snackbar.make(webView, R.string.fallback_weblogin_text, Snackbar.LENGTH_INDEFINITE) .setAction(R.string.fallback_weblogin_back, (View.OnClickListener) v -> initLegacyLogin(field_url.getText().toString())).show(); }, 45 * 1000); } private String getWebLoginUserAgent() { return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) + Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL; } private void parseAndLoginFromWebView(String dataString) { String prefix = "nc://login/"; LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, dataString); if (loginUrlInfo != null) { String url = normalizeUrlSuffix(loginUrlInfo.serverAddress); new LoginValidatorAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, loginUrlInfo.username, loginUrlInfo.password); } } /** * 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 */ private 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 String normalizeUrlSuffix(String url) { if (url.toLowerCase(Locale.ROOT).endsWith(WEBDAV_PATH_4_0_AND_LATER)) { return url.substring(0, url.length() - WEBDAV_PATH_4_0_AND_LATER.length()); } if (!url.endsWith("/")) { return url + "/"; } return url; } private void initLegacyLogin(String oldUrl) { useWebLogin = false; new URLValidatorAsyncTask().execute(NotesClientUtil.formatURL(field_url.getText().toString())); webView.setVisibility(View.INVISIBLE); setContentView(R.layout.activity_settings); ButterKnife.bind(this); setupListener(); field_url.setText(oldUrl); username_wrapper.setVisibility(View.VISIBLE); password_wrapper.setVisibility(View.VISIBLE); } private void handleSubmitButtonEnabled() { // drawable[2] is not null if url is valid, see URLValidatorAsyncTask::onPostExecute if (useWebLogin || field_url.getCompoundDrawables()[2] != null && (username_wrapper.getVisibility() == View.GONE || (username_wrapper.getVisibility() == View.VISIBLE && field_username.getText().length() > 0))) { btn_submit.setEnabled(true); } else { btn_submit.setEnabled(false); Loading @@ -203,6 +445,7 @@ public class SettingsActivity extends AppCompatActivity { @Override protected void onPreExecute() { btn_submit.setEnabled(false); field_url.setCompoundDrawables(null, null, null, null); } Loading @@ -221,6 +464,7 @@ public class SettingsActivity extends AppCompatActivity { } else { field_url.setCompoundDrawables(null, null, null, null); } handleSubmitButtonEnabled(); } } Loading Loading @@ -284,4 +528,13 @@ public class SettingsActivity extends AppCompatActivity { field_password.setEnabled(enabled); } } /** * Data object holding the login url fields. */ public class LoginUrlInfo { String serverAddress; String username; String password; } }
app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java +10 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.IBinder; import android.os.RemoteException; import android.preference.PreferenceManager; import android.util.Log; import android.widget.Toast; Loading @@ -27,6 +28,8 @@ import java.util.Set; import at.bitfire.cert4android.CustomCertManager; import at.bitfire.cert4android.CustomCertService; import at.bitfire.cert4android.ICustomCertService; import at.bitfire.cert4android.IOnCertificateDecision; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.android.activity.SettingsActivity; import it.niedermann.owncloud.notes.model.CloudNote; Loading Loading @@ -64,6 +67,7 @@ public class NoteServerSyncHelper { private final Context appContext; private CustomCertManager customCertManager; private ICustomCertService iCustomCertService; // Track network connection changes using a BroadcastReceiver private boolean networkConnected = false; Loading Loading @@ -94,6 +98,7 @@ public class NoteServerSyncHelper { private final ServiceConnection certService = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { iCustomCertService = ICustomCertService.Stub.asInterface(iBinder); cert4androidReady = true; if (isSyncPossible()) { scheduleSync(false); Loading @@ -103,6 +108,7 @@ public class NoteServerSyncHelper { @Override public void onServiceDisconnected(ComponentName componentName) { cert4androidReady = false; iCustomCertService = null; } }; Loading Loading @@ -168,6 +174,10 @@ public class NoteServerSyncHelper { return customCertManager; } public void checkCertificate(byte[] cert, boolean foreground, IOnCertificateDecision callback) throws RemoteException { iCustomCertService.checkTrusted(cert, true, foreground, callback); } /** * Adds a callback method to the NoteServerSyncHelper for the synchronization part push local changes to the server. * All callbacks will be executed once the synchronization operations are done. Loading
app/src/main/java/it/niedermann/owncloud/notes/util/SupportUtil.java +3 −2 Original line number Diff line number Diff line Loading @@ -4,13 +4,14 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; import androidx.annotation.WorkerThread; import android.text.Html; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.util.Log; import android.widget.TextView; import androidx.annotation.WorkerThread; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; Loading Loading @@ -93,6 +94,6 @@ public class SupportUtil { @WorkerThread public static CustomCertManager getCertManager(Context ctx) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); return new CustomCertManager(ctx, preferences.getBoolean(ctx.getString(R.string.pref_key_trust_system_certs), true)); return new CustomCertManager(ctx, preferences.getBoolean(ctx.getString(R.string.pref_key_trust_system_certs), true), true, true); } }