Compare commits

...

36 Commits

Author SHA1 Message Date
Ricki Hirner
c43f016dc7 Update Kotlin 2018-01-18 16:17:08 +01:00
Ricki Hirner
c45e4a1797 Version bump to 1.10 2018-01-18 16:14:23 +01:00
Ricki Hirner
3a734c9e68 Fetch translations from Transifex 2018-01-18 16:13:52 +01:00
Ricki Hirner
e0e4a026e6 Test for client certificates 2018-01-18 16:08:34 +01:00
Ricki Hirner
6f6182c0ce Managed: client certificates 2018-01-18 15:07:35 +01:00
Ricki Hirner
4158ec9aee iCloud, Soldupe 2018-01-16 13:38:52 +01:00
Ricki Hirner
c49333998f Login activity: add padding; sync: re-throw Interrupted(IO)Exception 2018-01-15 21:32:21 +01:00
Ricki Hirner
bc4f4b5dfd Version bump to 1.10-beta 2018-01-15 20:54:34 +01:00
Ricki Hirner
dbd5bde458 Fetch translations from Transifex 2018-01-15 20:52:33 +01:00
Ricki Hirner
be4c680497 Remove unnecessary strings 2018-01-15 20:49:28 +01:00
Ricki Hirner
d7c5ed23b7 Improve sync error notifications
* refactor checking for cancelled sync
* notify on SSLHandshakeException (except when a certificate was rejected by cert4android)
* show exception cause in debug info
2018-01-15 20:41:03 +01:00
Ricki Hirner
25a328a3c4 Login activity
* use TextInputLayout for input fields
* use support library instead of custom EditPassword widget
* improve client certificate UI
2018-01-15 13:58:21 +01:00
Ricki Hirner
dd3d95bdb9 Login with client certificates
* setup UI: login with URL and client certificate
* account settings UI: show either username/password or client certificate alias
* AccountSettings: serve credentials in generalized Credentials objects
* HttpClient: use Credentials (instead of username/password) for authentication
* HttpClient: always use CustomTlsSocketFactory
* CustomTlsSocketFactory: support client certificates
2018-01-13 22:56:33 +01:00
Ricki Hirner
0a47935430 Version bump to 1.9.10 2018-01-03 12:12:18 +01:00
Ricki Hirner
2a83f98f0a Version bump to 1.9.10-beta 2018-01-02 20:26:42 +01:00
Ricki Hirner
2224a6d2ae dav4android update 2018-01-02 20:26:38 +01:00
Ricki Hirner
30968f8ee3 Fetch translations from Transifex 2018-01-02 19:42:27 +01:00
Ricki Hirner
7f9be9a8da Fix DB upgrade logic 2018-01-02 19:25:51 +01:00
Ricki Hirner
a516800f45 Refactoring 2018-01-01 15:12:29 +01:00
Ricki Hirner
0c92b02d73 Do contact provider settings only at address book creation (fixes changed contact visibility at sync) 2017-12-31 13:37:46 +01:00
Ricki Hirner
95354096ab Version bump to 1.9.9 2017-12-27 13:06:22 +01:00
Ricki Hirner
d8a54f823b Fetch translations from Transifex 2017-12-26 13:07:17 +01:00
Ricki Hirner
ceedd218ca Version bump to 1.9.9-beta 2017-12-26 13:07:17 +01:00
Ricki Hirner
1627770103 Update for OpenTasks support
* ical4android update
* use uid field for tasks
* require OpenTasks 1.1.8.2
2017-12-26 13:07:09 +01:00
Ricki Hirner
82b1da5f0d Fix crash 2017-12-18 09:21:42 +01:00
Ricki Hirner
5e70b97942 Version bump to 1.9.8.1 2017-12-16 22:02:36 +01:00
Ricki Hirner
43e216642b Fetch translations from Transifex 2017-12-16 22:02:05 +01:00
Ricki Hirner
4d17bc673f Specify DNS server for dnsjava explicitly 2017-12-16 21:54:33 +01:00
Ricki Hirner
cf24bfa965 Version bump to 1.9.8 2017-12-16 19:08:49 +01:00
Ricki Hirner
3d746e7019 Fetch translations from Transifex 2017-12-16 19:08:49 +01:00
Ricki Hirner
9a30207316 Refactor Loaders, HttpClient
* improve Loader implementation
* use one shared HttpClient singleton
2017-12-16 19:08:49 +01:00
Ricki Hirner
dcb38e89a3 Navigation drawer: add link to manual 2017-12-16 19:08:45 +01:00
Ricki Hirner
2aed1ee97d Version bump to 1.9.7 2017-12-11 20:01:59 +01:00
Ricki Hirner
0a87a15822 Don't rely on LOGIN_ACCOUNTS_CHANGED_ACTION 2017-12-11 18:15:28 +01:00
Ricki Hirner
0bca883d76 Only ask for tasks permission if there's a tasks provider 2017-12-04 23:29:51 +01:00
Ricki Hirner
e4c282cd99 Use email instead of forum for beta feedback 2017-12-03 15:47:27 +01:00
69 changed files with 1366 additions and 842 deletions

View File

@@ -19,7 +19,7 @@ android {
applicationId "at.bitfire.davdroid"
resValue "string", "packageID", applicationId
versionCode 194
versionCode 203
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
minSdkVersion 19 // Android 4.4
@@ -37,13 +37,13 @@ android {
productFlavors {
standard {
dimension "type"
versionName "1.9.6"
versionName "1.10"
buildConfigField "boolean", "customCerts", "true"
}
managed {
dimension "type"
versionName "1.9.6-mgd"
versionName "1.10-mgd"
applicationId "com.davdroid.managed"
resValue "string", "packageID", applicationId
@@ -55,20 +55,20 @@ android {
gplay {
dimension "type"
versionName "1.9.6-gplay"
versionName "1.10-gplay"
buildConfigField "boolean", "customCerts", "true"
}
icloud {
dimension "type"
versionName "1.9.6-cloud"
versionName "1.10-cloud"
applicationId "at.bitfire.cloudsync"
resValue "string", "packageID", applicationId
}
soldupe {
dimension "type"
versionName "1.9.6-soldupe"
versionName "1.10-soldupe"
applicationId "com.soldupe.cloudsync"
resValue "string", "packageID", applicationId
@@ -137,7 +137,7 @@ dependencies {
compile project(':ical4android')
compile project(':vcard4android')
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
compile 'com.android.support:appcompat-v7:27.0.2'
compile 'com.android.support:cardview-v7:27.0.2'

View File

@@ -0,0 +1,128 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid;
import android.support.test.InstrumentationRegistry.getInstrumentation
import at.bitfire.cert4android.CustomCertManager
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.mockwebserver.MockWebServer
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.ArrayUtils.contains
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import java.net.Socket
import java.security.KeyFactory
import java.security.KeyStore
import java.security.Principal
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.PKCS8EncodedKeySpec
import javax.net.ssl.SSLSocket
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509ExtendedKeyManager
import javax.net.ssl.X509TrustManager
class CustomTlsSocketFactoryTest {
lateinit var certMgr: CustomCertManager
lateinit var factory: CustomTlsSocketFactory
val server = MockWebServer()
@Before
fun startServer() {
certMgr = CustomCertManager(getInstrumentation().context, false, true)
factory = CustomTlsSocketFactory(null, certMgr)
server.start()
}
@After
fun stopServer() {
server.shutdown()
certMgr.close()
}
@Test
fun testSendClientCertificate() {
var public: X509Certificate? = null
javaClass.classLoader.getResourceAsStream("sample.crt").use {
public = CertificateFactory.getInstance("X509").generateCertificate(it) as? X509Certificate
}
assertNotNull(public)
val keyFactory = KeyFactory.getInstance("RSA")
val private = keyFactory.generatePrivate(PKCS8EncodedKeySpec(readResource("sample.key")))
assertNotNull(private)
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val alias = "sample"
keyStore.setKeyEntry(alias, private, null, arrayOf(public))
assertTrue(keyStore.containsAlias(alias))
val trustManagerFactory = TrustManagerFactory.getInstance("X509")
trustManagerFactory.init(null as KeyStore?)
val trustManager = trustManagerFactory.trustManagers.first() as X509TrustManager
val factory = CustomTlsSocketFactory(object: X509ExtendedKeyManager() {
override fun getServerAliases(p0: String?, p1: Array<out Principal>?): Array<String>? = null
override fun chooseServerAlias(p0: String?, p1: Array<out Principal>?, p2: Socket?) = null
override fun getClientAliases(p0: String?, p1: Array<out Principal>?) =
arrayOf(alias)
override fun chooseClientAlias(p0: Array<out String>?, p1: Array<out Principal>?, p2: Socket?) =
alias
override fun getCertificateChain(forAlias: String?) =
arrayOf(public).takeIf { forAlias == alias }
override fun getPrivateKey(forAlias: String?) =
private.takeIf { forAlias == alias }
}, trustManager)
/* known client cert test URLs (thanks!):
* - https://prod.idrix.eu/secure/
* - https://server.cryptomix.com/secure/
*/
val client = OkHttpClient.Builder()
.sslSocketFactory(factory, trustManager)
.build()
client.newCall(Request.Builder()
.get()
.url("https://prod.idrix.eu/secure/")
.build()).execute().use { response ->
assertTrue(response.isSuccessful)
assertTrue(response.body()!!.string().contains("CN=User Cert,O=Internet Widgits Pty Ltd,ST=Some-State,C=CA"))
}
}
@Test
fun testUpgradeTLS() {
val s = factory.createSocket(server.hostName, server.port)
assertTrue(s is SSLSocket)
val ssl = s as SSLSocket
assertFalse(contains(ssl.enabledProtocols, "SSLv3"))
assertTrue(contains(ssl.enabledProtocols, "TLSv1"))
assertTrue(contains(ssl.enabledProtocols, "TLSv1.1"))
assertTrue(contains(ssl.enabledProtocols, "TLSv1.2"))
}
private fun readResource(name: String): ByteArray {
this.javaClass.classLoader.getResourceAsStream(name).use {
return IOUtils.toByteArray(it)
}
}
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.net.Socket;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import at.bitfire.cert4android.CustomCertManager;
import okhttp3.mockwebserver.MockWebServer;
import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static android.support.test.InstrumentationRegistry.getTargetContext;
import static junit.framework.TestCase.assertFalse;
import static org.apache.commons.lang3.ArrayUtils.contains;
import static org.junit.Assert.assertTrue;
public class SSLSocketFactoryCompatTest {
CustomCertManager certMgr;
SSLSocketFactoryCompat factory;
MockWebServer server = new MockWebServer();
@Before
public void startServer() throws Exception {
certMgr = new CustomCertManager(getInstrumentation().getContext(), false, true);
factory = new SSLSocketFactoryCompat(certMgr);
server.start();
}
@After
public void stopServer() throws Exception {
server.shutdown();
certMgr.close();
}
@Test
public void testUpgradeTLS() throws IOException {
Socket s = factory.createSocket(server.getHostName(), server.getPort());
assertTrue(s instanceof SSLSocket);
SSLSocket ssl = (SSLSocket)s;
assertFalse(contains(ssl.getEnabledProtocols(), "SSLv3"));
assertTrue(contains(ssl.getEnabledProtocols(), "TLSv1"));
assertTrue(contains(ssl.getEnabledProtocols(), "TLSv1.1"));
assertTrue(contains(ssl.getEnabledProtocols(), "TLSv1.2"));
}
}

View File

@@ -22,6 +22,7 @@ import at.bitfire.dav4android.property.AddressbookHomeSet;
import at.bitfire.dav4android.property.ResourceType;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.log.Logger;
import at.bitfire.davdroid.model.Credentials;
import at.bitfire.davdroid.ui.setup.DavResourceFinder.Configuration.ServiceInfo;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
@@ -40,7 +41,7 @@ public class DavResourceFinderTest {
DavResourceFinder finder;
HttpClient client;
LoginCredentials credentials;
LoginInfo loginInfo;
private static final String
PATH_NO_DAV = "/nodav",
@@ -58,11 +59,11 @@ public class DavResourceFinderTest {
server.setDispatcher(new TestDispatcher());
server.start();
credentials = new LoginCredentials(URI.create("/"), "mock", "12345");
finder = new DavResourceFinder(getTargetContext(), credentials);
loginInfo = new LoginInfo(URI.create("/"), new Credentials("mock", "12345"));
finder = new DavResourceFinder(getTargetContext(), loginInfo);
client = new HttpClient.Builder()
.addAuthentication(null, credentials.getUserName(), credentials.getPassword())
.addAuthentication(null, loginInfo.credentials)
.build();
}

View File

@@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEEzCCAfsCAQEwDQYJKoZIhvcNAQEFBQAwRjELMAkGA1UEBhMCQ0ExEzARBgNV
BAgMClNvbWUtU3RhdGUxEDAOBgNVBAoMB0NBIENlcnQxEDAOBgNVBAMMB0NBIENl
cnQwHhcNMTgwMTEzMjAyOTI5WhcNMTkwMTEzMjAyOTI5WjBZMQswCQYDVQQGEwJD
QTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
cyBQdHkgTHRkMRIwEAYDVQQDDAlVc2VyIENlcnQwggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQDqOyHAeG4psE/f6i/eTfwbhn6j7WaFXxZiSOWwpQZmzRrx
MrfkABJCk0X7KNgCaJcmBkG9G1Ri4HfKrxvJFswMXknlq+0ulGBk7oDnZM+pihuX
3D9VCWMMkCqYhLCGADj2zB2mkX4LpcMRi6XoOetKURE/vcIy7rSLAtJM6ZRdftfh
2ZxnautS1Tyujh9Au3NI/+Of80tT/nA+oBJQeT1fB/ga1OQlZP5kjSaA7IPiIbTz
QBO+r898MvqK/lwsvOYnWAp7TY03z+vPfCs0zjijZEl9Wrl0hW6o5db5kU1v5bcr
p87hxFJsGD2HIr2y6kvYfL2hn+h9iANyYdRnUgapAgMBAAEwDQYJKoZIhvcNAQEF
BQADggIBAHANsiJITedXPyp89lVMEmGY3zKtOqgQ3tqjvjlNt2sdPnj7wmZbmrNd
sa90S/UwOn8PzEFOVxYy1BPlljlEjtjmc4OHMcm4P4Zv36uawHilmK8V+zT59gCK
ftB5FP2TLFUFi2X9o8J06d0xJRE77uewN155NV4RmPuP4b/tMmeixoQppHqLqEr5
lgEUnt3Mh1ctmeFQFJR6lJ01hlB0gdpVHIhzrVLTO3uo8ePLJTmxP6tyKl/HXj9F
mpVsKb1kriKwbkGczfw99OUZeUVbTwQOR07r0SrG71B7IuDvxIORnhQc1OUjt7ob
wjdaZauAHxpGBRu+hw9Yqaxchk9Gldy1nEjGyyVCD0FU5taXbl8PhBWEDc4U9tI+
xVNmPpsSuCsbz3Mjd1YIVRGL99vLrKsQcj+TNM+jJKKRKes3ihl+l/0FwG6UuO7L
EvjlUg5hOtYi1D7xuYyMjroGBGh7swYMt6w4eCDbcjzcCkaCi0H2pScM/rLBpDjS
LIoGCvZ1LBdi933/iOj1/8dxGZwY6fEgcyiD2n0xAgYIniLWjEZXOMdIK5FNTNga
Tswanvp+6Noa4oIu/hl/LXvPMsouaWfSEbRe0Dshi3GpLj3YtEHoN9DHB8bn7jy5
34By81GT41m5kq3hWP//x9kSHYSADpbovCbKbElU1qSt6vTVR4nq
-----END CERTIFICATE-----

View File

Binary file not shown.

View File

@@ -20,6 +20,11 @@ import at.bitfire.davdroid.settings.ISettings
class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
companion object {
private val BETA_FEEDBACK_URI = "mailto:support@davdroid.com?subject=${BuildConfig.APPLICATION_ID} beta feedback ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
}
override fun onSettingsChanged(settings: ISettings?, menu: Menu) {
if (BuildConfig.VERSION_NAME.contains("-beta") || BuildConfig.VERSION_NAME.contains("-rc"))
menu.findItem(R.id.nav_beta_feedback).isVisible = true
@@ -31,14 +36,21 @@ class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
activity.startActivity(Intent(activity, AboutActivity::class.java))
R.id.nav_app_settings ->
activity.startActivity(Intent(activity, AppSettingsActivity::class.java))
R.id.nav_beta_feedback ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.beta_feedback_url))))
R.id.nav_beta_feedback -> {
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse(BETA_FEEDBACK_URI))
if (activity.packageManager.resolveActivity(intent, 0) != null)
activity.startActivity(intent)
}
R.id.nav_twitter ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/davdroidapp")))
R.id.nav_website ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))))
R.id.nav_manual ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
.buildUpon().appendEncodedPath("manual/").build()))
R.id.nav_faq ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.navigation_drawer_faq_url))))
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
.buildUpon().appendEncodedPath("faq/").build()))
R.id.nav_forums ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
.buildUpon().appendEncodedPath("forums/").build()))

View File

@@ -11,10 +11,15 @@ package at.bitfire.davdroid.ui.setup
import android.app.Fragment
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.security.KeyChain
import android.security.KeyChainAliasCallback
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.Toast
import at.bitfire.dav4android.Constants
import at.bitfire.davdroid.R
import kotlinx.android.synthetic.standard.login_credentials_fragment.view.*
@@ -37,10 +42,10 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
val password = it.getStringExtra(LoginActivity.EXTRA_PASSWORD)
if (url != null) {
v.login_type_url.isChecked = true
v.base_url.setText(url)
v.user_name.setText(username)
v.url_password.setText(password)
v.login_type_urlpwd.isChecked = true
v.urlpwd_base_url.setText(url)
v.urlpwd_user_name.setText(username)
v.urlpwd_password.setText(password)
} else {
v.login_type_email.isChecked = true
v.email_address.setText(username)
@@ -49,17 +54,27 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
}
}
v.login.setOnClickListener({ _ ->
validateLoginData()?.let { credentials ->
DetectConfigurationFragment.newInstance(credentials).show(fragmentManager, null)
v.urlcert_select_cert.setOnClickListener {
KeyChain.choosePrivateKeyAlias(activity, KeyChainAliasCallback { alias ->
Handler(Looper.getMainLooper()).post({
v.urlcert_cert_alias.text = alias
v.urlcert_cert_alias.error = null
})
}, null, null, null, -1, view.urlcert_cert_alias.text.toString())
}
v.login.setOnClickListener {
validateLoginData()?.let { info ->
DetectConfigurationFragment.newInstance(info).show(fragmentManager, null)
}
})
}
// initialize to Login by email
onCheckedChanged(v)
v.login_type_email.setOnCheckedChangeListener(this)
v.login_type_url.setOnCheckedChangeListener(this)
v.login_type_urlpwd.setOnCheckedChangeListener(this)
v.login_type_urlcert.setOnCheckedChangeListener(this)
return v
}
@@ -69,92 +84,124 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
}
private fun onCheckedChanged(v: View) {
val loginByEmail = !v.login_type_url.isChecked
v.login_type_email_details.visibility = if (loginByEmail) View.VISIBLE else View.GONE
v.login_type_url_details.visibility = if (loginByEmail) View.GONE else View.VISIBLE
(if (loginByEmail) v.email_address else v.base_url).requestFocus()
v.login_type_email_details.visibility = if (v.login_type_email.isChecked) View.VISIBLE else View.GONE
v.login_type_urlpwd_details.visibility = if (v.login_type_urlpwd.isChecked) View.VISIBLE else View.GONE
v.login_type_urlcert_details.visibility = if (v.login_type_urlcert.isChecked) View.VISIBLE else View.GONE
}
private fun validateLoginData(): LoginCredentials? {
if (view.login_type_email.isChecked) {
var uri: URI? = null
var valid = true
private fun validateLoginData(): LoginInfo? {
when {
// Login with email address
view.login_type_email.isChecked -> {
var uri: URI? = null
var valid = true
val email = view.email_address.text.toString()
if (!email.matches(Regex(".+@.+"))) {
view.email_address.error = getString(R.string.login_email_address_error)
valid = false
} else
try {
uri = URI("mailto", email, null)
} catch(e: URISyntaxException) {
view.email_address.error = e.localizedMessage
valid = false
}
val password = view.email_password.getText().toString()
if (password.isEmpty()) {
view.email_password.setError(getString(R.string.login_password_required))
valid = false
}
return if (valid && uri != null)
LoginCredentials(uri, email, password)
else
null
} else if (view.login_type_url.isChecked) {
var uri: URI? = null
var valid = true
val baseUrl = Uri.parse(view.base_url.text.toString())
val scheme = baseUrl.scheme
if (scheme.equals("http", true) || scheme.equals("https", true)) {
var host = baseUrl.host
if (host.isNullOrBlank()) {
view.base_url.error = getString(R.string.login_url_host_name_required)
val email = view.email_address.text.toString()
if (!email.matches(Regex(".+@.+"))) {
view.email_address.error = getString(R.string.login_email_address_error)
valid = false
} else
try {
host = IDN.toASCII(host)
} catch(e: IllegalArgumentException) {
Constants.log.log(Level.WARNING, "Host name not conforming to RFC 3490", e)
uri = URI("mailto", email, null)
} catch (e: URISyntaxException) {
view.email_address.error = e.localizedMessage
valid = false
}
val path = baseUrl.encodedPath
val port = baseUrl.port
try {
uri = URI(baseUrl.scheme, null, host, port, path, null, null)
} catch(e: URISyntaxException) {
view.base_url.error = e.localizedMessage
val password = view.email_password.getText().toString()
if (password.isEmpty()) {
view.email_password.error = getString(R.string.login_password_required)
valid = false
}
} else {
view.base_url.error = getString(R.string.login_url_must_be_http_or_https)
valid = false
return if (valid && uri != null)
LoginInfo(uri, email, password)
else
null
}
val userName = view.user_name.text.toString()
if (userName.isBlank()) {
view.user_name.error = getString(R.string.login_user_name_required)
valid = false
// Login with URL and user name
view.login_type_urlpwd.isChecked -> {
var valid = true
val baseUrl = Uri.parse(view.urlpwd_base_url.text.toString())
val uri = validateBaseUrl(baseUrl, false, { message ->
view.urlpwd_base_url.error = message
valid = false
})
val userName = view.urlpwd_user_name.text.toString()
if (userName.isBlank()) {
view.urlpwd_user_name.error = getString(R.string.login_user_name_required)
valid = false
}
val password = view.urlpwd_password.text.toString()
if (password.isEmpty()) {
view.urlpwd_password.error = getString(R.string.login_password_required)
valid = false
}
return if (valid && uri != null)
LoginInfo(uri, userName, password)
else
null
}
val password = view.url_password.getText().toString()
if (password.isEmpty()) {
view.url_password.setError(getString(R.string.login_password_required))
valid = false
}
// Login with URL and client certificate
view.login_type_urlcert.isChecked -> {
var valid = true
return if (valid && uri != null)
LoginCredentials(uri, userName, password)
else
null
val baseUrl = Uri.parse(view.urlcert_base_url.text.toString())
val uri = validateBaseUrl(baseUrl, true, { message ->
view.urlcert_base_url.error = message
valid = false
})
val alias = view.urlcert_cert_alias.text.toString()
if (alias.isEmpty()) {
view.urlcert_cert_alias.error = ""
valid = false
}
if (valid && uri != null)
return LoginInfo(uri, certificateAlias = alias)
}
}
return null
}
private fun validateBaseUrl(baseUrl: Uri, httpsRequired: Boolean, reportError: (String) -> Unit): URI? {
var uri: URI? = null
val scheme = baseUrl.scheme
if ((!httpsRequired && scheme.equals("http", true)) || scheme.equals("https", true)) {
var host = baseUrl.host
if (host.isNullOrBlank())
reportError(getString(R.string.login_url_host_name_required))
else
try {
host = IDN.toASCII(host)
} catch (e: IllegalArgumentException) {
Constants.log.log(Level.WARNING, "Host name not conforming to RFC 3490", e)
}
val path = baseUrl.encodedPath
val port = baseUrl.port
try {
uri = URI(baseUrl.scheme, null, host, port, path, null, null)
} catch (e: URISyntaxException) {
reportError(e.localizedMessage)
}
} else
reportError(getString(if (httpsRequired)
R.string.login_url_must_be_https
else
R.string.login_url_must_be_http_or_https))
return uri
}
class Factory: ILoginCredentialsFragment {

View File

@@ -9,6 +9,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -38,61 +39,140 @@
android:id="@+id/login_type_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/login_type_email"
android:paddingLeft="14dp" tools:ignore="RtlSymmetry"
style="@style/login_type_headline"/>
<LinearLayout
android:id="@+id/login_type_email_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:orientation="vertical">
<EditText
android:id="@+id/email_address"
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TextInputEditText
android:id="@+id/email_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_email_address"
android:inputType="textEmailAddress"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_email_address"
android:inputType="textEmailAddress"/>
<at.bitfire.davdroid.ui.widget.EditPassword
android:id="@+id/email_password"
android:hint="@string/login_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
app:passwordToggleEnabled="true">
<android.support.design.widget.TextInputEditText
android:id="@+id/email_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_password"
android:fontFamily="monospace"
android:inputType="textPassword"/>
</android.support.design.widget.TextInputLayout>
</LinearLayout>
<RadioButton
android:id="@+id/login_type_url"
android:id="@+id/login_type_urlpwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_type_url"
android:layout_marginTop="16dp"
android:paddingLeft="14dp" tools:ignore="RtlSymmetry"
style="@style/login_type_headline"/>
<LinearLayout
android:id="@+id/login_type_url_details"
android:id="@+id/login_type_urlpwd_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:orientation="vertical">
<EditText
android:id="@+id/base_url"
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TextInputEditText
android:id="@+id/urlpwd_base_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_base_url"
android:inputType="textUri"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TextInputEditText
android:id="@+id/urlpwd_user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_user_name"
android:inputType="textEmailAddress"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_base_url"
android:inputType="textUri"/>
<EditText
android:id="@+id/user_name"
app:passwordToggleEnabled="true">
<android.support.design.widget.TextInputEditText
android:id="@+id/urlpwd_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:inputType="textPassword"
android:hint="@string/login_password"/>
</android.support.design.widget.TextInputLayout>
</LinearLayout>
<RadioButton
android:id="@+id/login_type_urlcert"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_type_url_certificate"
android:paddingLeft="14dp" tools:ignore="RtlSymmetry"
style="@style/login_type_headline"/>
<LinearLayout
android:id="@+id/login_type_urlcert_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:orientation="vertical">
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_user_name"
android:inputType="textEmailAddress"/>
<at.bitfire.davdroid.ui.widget.EditPassword
android:id="@+id/url_password"
android:layout_height="wrap_content">
<android.support.design.widget.TextInputEditText
android:id="@+id/urlcert_base_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_base_url"
android:inputType="textUri"/>
</android.support.design.widget.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_password"/>
android:layout_height="wrap_content">
<TextView
android:id="@+id/urlcert_cert_alias"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:paddingLeft="3dp"
android:paddingRight="3dp"
style="@style/Base.TextAppearance.AppCompat.Body1"
android:textSize="16sp"/>
<Button
android:id="@+id/urlcert_select_cert"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:text="@string/login_select_certificate"/>
</LinearLayout>
</LinearLayout>

View File

@@ -88,6 +88,8 @@
<string name="account_carddav">CardDAV (les carnets d\'adresse) </string>
<string name="account_caldav">CalDAV (les agendas) </string>
<string name="account_webcal">WebCal (les anciens agenda)</string>
<string name="account_calendar">calendrier</string>
<string name="account_task_list">liste de tâche</string>
<string name="account_refresh_address_book_list">Actualiser le carnet d\'adresses</string>
<string name="account_create_new_address_book">Créer un nouveau carnet d\'adresses</string>
<string name="account_refresh_calendar_list">Actualiser le calendrier</string>
@@ -156,7 +158,7 @@
<string name="settings_sync_wifi_only">Synchronisation en Wifi seulement</string>
<string name="settings_sync_wifi_only_on">La synchronisation est limitée aux connexions WiFi</string>
<string name="settings_sync_wifi_only_off">Le type de connexion n\'est pas pris en charge</string>
<string name="settings_sync_wifi_only_ssids">Restrictions concernant le nom de réseau WiFi (SSID)</string>
<string name="settings_sync_wifi_only_ssids">Restriction WiFi SSID</string>
<string name="settings_sync_wifi_only_ssids_on">Synchronisation possible seulement en %s</string>
<string name="settings_sync_wifi_only_ssids_off">Toutes les connexions WiFi seront utilisées</string>
<string name="settings_sync_wifi_only_ssids_message">Liste des points d\'accès WiFi (SSID) autorisés, séparés par des virgules. (Laissez vide pour tous)</string>

View File

@@ -10,6 +10,9 @@
<string name="send">送信</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">デバッグ中</string>
<string name="notification_channel_sync_status">同期ステータス</string>
<string name="notification_channel_sync_problems">同期の問題</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">バッテリー最適化</string>
<string name="startup_battery_optimization_message">Android は数日後に DAVdroid の同期を無効にする/減らすことがあります。これを防止するには、バッテリー最適化をオフにしてください。</string>
@@ -44,8 +47,8 @@
<string name="navigation_drawer_news_updates">ニュース &amp; 更新</string>
<string name="navigation_drawer_external_links">外部リンク</string>
<string name="navigation_drawer_website">Web サイト</string>
<string name="navigation_drawer_manual">手動</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_faq_url">https://www.davdroid.com/faq/?pk_campaign=davdroid-app</string>
<string name="navigation_drawer_forums">ヘルプ / フォーラム</string>
<string name="navigation_drawer_donate">寄付</string>
<string name="account_list_empty">DAVdroid にようこそ!\n\nCalDAV/CardDAV アカウントを追加できるようになりました。</string>
@@ -124,10 +127,13 @@
<string name="login_password_required">パスワードが必要です</string>
<string name="login_type_url">URL とユーザー名でログイン</string>
<string name="login_url_must_be_http_or_https">URL は http(s):// で始まる必要があります</string>
<string name="login_url_must_be_https">URL は https:// で始まる必要があります</string>
<string name="login_url_host_name_required">ホスト名が必要です</string>
<string name="login_user_name">ユーザー名</string>
<string name="login_user_name_required">ユーザー名が必要です</string>
<string name="login_base_url">ベース URL</string>
<string name="login_type_url_certificate">URL とクライアント証明書でログイン</string>
<string name="login_select_certificate">証明書を選択</string>
<string name="login_login">ログイン</string>
<string name="login_back">戻る</string>
<string name="login_create_account">アカウントを作成</string>
@@ -148,6 +154,7 @@
<string name="settings_password">パスワード</string>
<string name="settings_password_summary">ご利用のサーバーに従ってパスワードを更新します。</string>
<string name="settings_enter_password">パスワードを入力:</string>
<string name="settings_certificate_alias">クライアント証明書の別名</string>
<string name="settings_sync">同期</string>
<string name="settings_sync_interval_contacts">連絡先同期間隔</string>
<string name="settings_sync_summary_manually">手動のみ</string>
@@ -217,6 +224,7 @@
<string name="delete_collection_confirm_title">よろしいですか?</string>
<string name="delete_collection_confirm_warning">このコレクション (%s) とそのすべてのデータがサーバーから削除されます。</string>
<string name="delete_collection_deleting_collection">コレクションの削除中</string>
<string name="collection_force_read_only">強制的に読み取り専用</string>
<!--ExceptionInfoFragment-->
<string name="exception">エラーが発生しました。</string>
<string name="exception_httpexception">HTTP エラーが発生しました。</string>
@@ -230,6 +238,8 @@
</plurals>
<string name="sync_error_permissions">DAVdroid アクセス許可</string>
<string name="sync_error_permissions_text">追加のアクセス許可が必要です</string>
<string name="sync_error_opentasks_too_old">OpenTasks が古すぎます</string>
<string name="sync_error_opentasks_required_version">必要なバージョン: %1$s (現在 %2$s)</string>
<string name="sync_error_calendar">カレンダーの同期に失敗しました (%s)</string>
<string name="sync_error_contacts">アドレス帳の同期に失敗しました (%s)</string>
<string name="sync_error_tasks">タスクの同期に失敗しました (%s)</string>

View File

@@ -45,7 +45,6 @@
<string name="navigation_drawer_external_links">Eksterne lenker</string>
<string name="navigation_drawer_website">Nettside</string>
<string name="navigation_drawer_faq">O-S-S</string>
<string name="navigation_drawer_faq_url">https://www.davdroid.com/faq/?pk_campaign=davdroid-app</string>
<string name="navigation_drawer_forums">Hjelp / Forum</string>
<string name="navigation_drawer_donate">Doner</string>
<string name="account_list_empty">Velkommen til DAVdroid.\n\nDu kan legge til en CalDAV/CardDAV-konto nå.</string>
@@ -218,6 +217,7 @@
<string name="delete_collection_confirm_title">Er du sikker?</string>
<string name="delete_collection_confirm_warning">Denne samlingen (%s) og all dens data vil bli fjernet fra tjeneren.</string>
<string name="delete_collection_deleting_collection">Sletter samling</string>
<string name="collection_force_read_only">Tving kun lesbar</string>
<!--ExceptionInfoFragment-->
<string name="exception">En feil har inntruffet</string>
<string name="exception_httpexception">En HTTP-feil har inntruffet.</string>

View File

@@ -45,7 +45,6 @@
<string name="navigation_drawer_external_links">Zewnętrzne odnośniki</string>
<string name="navigation_drawer_website">Strona WWW</string>
<string name="navigation_drawer_faq">Pytania i odpowiedzi</string>
<string name="navigation_drawer_faq_url">https://www.davdroid.com/faq/?pk_campaign=davdroid-app</string>
<string name="navigation_drawer_forums">Pomoc / Forum</string>
<string name="navigation_drawer_donate">Dotacja</string>
<string name="account_list_empty">Witamy w DAVdroid!\n\nMożesz teraz dodać konto CalDAV/CardDAV.</string>

View File

@@ -8,6 +8,9 @@
<string name="manage_accounts">Gerenciar contas</string>
<string name="please_wait">Por favor, aguarde...</string>
<string name="send">Enviar</string>
<string name="notification_channel_debugging">Depuração</string>
<string name="notification_channel_sync_status">Status da sincronização</string>
<string name="notification_channel_sync_problems">Problemas de sincronização</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Otimização da bateria</string>
<string name="startup_battery_optimization_message">O Android pode desativar/reduzir a sincronização do DAVdroid depois de alguns dias. Para evitar isso, desligue a otimização da bateria.</string>
@@ -42,6 +45,7 @@
<string name="navigation_drawer_news_updates">Novidades e atualizações</string>
<string name="navigation_drawer_external_links">Links externos</string>
<string name="navigation_drawer_website">Site na Web</string>
<string name="navigation_drawer_manual">Manual</string>
<string name="navigation_drawer_faq">Perguntas fequentes</string>
<string name="navigation_drawer_forums">Ajuda / Fóruns</string>
<string name="navigation_drawer_donate">Doações</string>
@@ -120,10 +124,13 @@
<string name="login_password_required">É necessário uma senha</string>
<string name="login_type_url">Autenticação com usuário e URL</string>
<string name="login_url_must_be_http_or_https">A URL deve começar com http(s)://</string>
<string name="login_url_must_be_https">A URL deve começar com https://</string>
<string name="login_url_host_name_required">É necessário um nome de máquina</string>
<string name="login_user_name">Usuário</string>
<string name="login_user_name_required">É necessário um nome de usuário</string>
<string name="login_base_url">URL base</string>
<string name="login_type_url_certificate">Autenticação com URL e certificado do cliente</string>
<string name="login_select_certificate">Selecionar certificado</string>
<string name="login_login">Autenticar</string>
<string name="login_back">Voltar</string>
<string name="login_create_account">Criar conta</string>
@@ -144,6 +151,7 @@
<string name="settings_password">Senha</string>
<string name="settings_password_summary">Atualize a senha de acordo com seu servidor</string>
<string name="settings_enter_password">Digite sua senha:</string>
<string name="settings_certificate_alias">Nome do certificado do cliente</string>
<string name="settings_sync">Sincronização</string>
<string name="settings_sync_interval_contacts">Intervalo sinc. de contatos</string>
<string name="settings_sync_summary_manually">Apenas manualmente</string>
@@ -229,6 +237,8 @@
</plurals>
<string name="sync_error_permissions">Permissões do DAVdroid</string>
<string name="sync_error_permissions_text">É necessário permissões adicionais</string>
<string name="sync_error_opentasks_too_old">A versão do OpenTasks é muito antiga</string>
<string name="sync_error_opentasks_required_version">Versão necessária: %1$s (atual %2$s)</string>
<string name="sync_error_calendar">Falha na sincronização do calendário (%s)</string>
<string name="sync_error_contacts">Falha na sincronização do livro de endereços (%s)</string>
<string name="sync_error_tasks">Falha na sincronização das tarefas (%s)</string>

View File

@@ -2,56 +2,68 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">Адресная книга DAVdroid</string>
<string name="address_books_authority_title">Адресные книги</string>
<string name="help">Помощь</string>
<string name="manage_accounts">Управление аккаунтами</string>
<string name="please_wait">Пожалуйста подождите...</string>
<string name="please_wait">Пожалуйста, подождите</string>
<string name="send">Отправить</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">Отладка</string>
<string name="notification_channel_sync_status">Статус синхронизации</string>
<string name="notification_channel_sync_problems">Проблемы с синхронизацией</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Оптимизация батареи</string>
<string name="startup_battery_optimization_message">Андроид может отключить/уменьшить синхронизацию DAVdroid через несколько дней. Чтобы этого не произошло, выключите оптимизацию батареи.</string>
<string name="startup_battery_optimization_message">Андроид может отключить/понизить синхронизацию DAVdroid через несколько дней. Чтобы это предотвратить, отключите оптимизацию батареи.</string>
<string name="startup_battery_optimization_disable">Отключить для DAVdroid</string>
<string name="startup_dont_show_again">Больше не показывать</string>
<string name="startup_dont_show_again">Не показывать снова</string>
<string name="startup_donate">Open-Source информация</string>
<string name="startup_donate_message">Мы рады, что вы используете DAVdroid, который является программным обеспечением с открытым исходным кодом (GPLv3). Разработка DAVdroid является сложной задачей, потребовавшей от нас тысяч рабочих часов. Пожалуйста, рассмотрите возможность поддержать проект.</string>
<string name="startup_donate_now">Поддержать проект</string>
<string name="startup_donate_message">Мы рады, что вы используете DAVdroid, который является программным обеспечением с открытым исходным кодом (GPLv3). Поскольку разработка DAVdroid - тяжелая работа и заняла у нас нас очень много времени, пожалуйста, рассмотрите возможность поддержать проект.</string>
<string name="startup_donate_now">Показать страницу пожертвования</string>
<string name="startup_donate_later">Возможно, позже</string>
<string name="startup_google_play_accounts_removed">Информация об ошибке в Play Store DRM</string>
<string name="startup_google_play_accounts_removed_message">При определённых условиях Play Store DRM может стать причиной потери всех DAVdroid аккаунтов после перезагрузки устройства или после обновления DAVdroid. Если Вы столкнулись с этой проблемой (и только в этом случае), установите \"DAVdroid JB Workaround\" из Play Store.</string>
<string name="startup_google_play_accounts_removed_more_info">Дополнительно</string>
<string name="startup_google_play_accounts_removed_message">При определенных условиях Play Store DRM может стать причиной потери всех DAVdroid аккаунтов после перезагрузки устройства или после обновления DAVdroid. Если Вы столкнулись с этой проблемой (и только в этом случае), установите \"DAVdroid JB Workaround\" из Play Store.</string>
<string name="startup_google_play_accounts_removed_more_info">Дополнительная информация</string>
<string name="startup_opentasks_not_installed">OpenTasks не установлен</string>
<string name="startup_opentasks_not_installed_message">OpenTasks недоступен, DAVdroid не сможет синхронизировать список задач</string>
<string name="startup_opentasks_reinstall_davdroid">После установки OpenTasks необходимо ПЕРЕУСТАНОВИТЬ DAVdroid и добавить Ваши аккаунты ещё раз (ошибка Android).</string>
<string name="startup_opentasks_not_installed_message">Приложение OpenTasks не установлено, поэтому DAVdroid не сможет синхронизировать список задач</string>
<string name="startup_opentasks_reinstall_davdroid">После установки OpenTasks необходимо ПЕРЕУСТАНОВИТЬ DAVdroid и повторно добавить ваши аккаунты (проблема Android).</string>
<string name="startup_opentasks_not_installed_install">Установить OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Лицензионное соглашение</string>
<string name="about_license_info_no_warranty">Эта программа поставляется АБСОЛЮТНО БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ. Это свободная программа, и вы приглашаетесь повторно распространять ее при определенных условиях.</string>
<string name="about_license_terms">Условия лицензии</string>
<string name="about_license_info_no_warranty">Эта программа поставляется АБСОЛЮТНО БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ. Это свободное программное обеспечение и вы можете передавать его при соблюдении определенных условий.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">Файл логов DAVdroid</string>
<string name="logging_davdroid_file_logging">Файл журнала DAVdroid</string>
<string name="logging_to_external_storage">Сохранение логов во внешнем хранилище: %s</string>
<string name="logging_couldnt_create_file">Не удалось создать внешний лог файл: %s</string>
<string name="logging_couldnt_create_file">Не удалось создать внешний файл журнала: %s</string>
<string name="logging_no_external_storage">Внешнее хранилище не найдено</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Открыть панель навигации</string>
<string name="navigation_drawer_close">Закрыть панель навигации</string>
<string name="navigation_drawer_subtitle">Адаптер синхронизации CalDAV/CardDAV</string>
<string name="navigation_drawer_about">О программе / Лицензия</string>
<string name="navigation_drawer_beta_feedback">Отзыв о бета-тестировании</string>
<string name="navigation_drawer_settings">Настройки</string>
<string name="navigation_drawer_news_updates">Новости и обновления</string>
<string name="navigation_drawer_news_updates">Новости &amp; обновления</string>
<string name="navigation_drawer_external_links">Внешние ссылки</string>
<string name="navigation_drawer_website">Веб сайт</string>
<string name="navigation_drawer_faq">ЧАВО</string>
<string name="navigation_drawer_website">Веб-сайт</string>
<string name="navigation_drawer_manual">Руководство</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_forums">Помощь / Форумы</string>
<string name="navigation_drawer_donate">Пожертвовать</string>
<string name="account_list_empty">Вас приветствует DAVdroid\n\nМожете добавить CalDAV/CardDAV аккаунт сейчас.</string>
<string name="account_list_empty">Добро пожаловать в DAVdroid\n\nТеперь вы можете добавить аккаунт CalDAV/CardDAV.</string>
<string name="accounts_global_sync_disabled">Синхронизация отключена на уровне устройства</string>
<string name="accounts_global_sync_enable">Включить</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Не удалось обнаружить сервисы</string>
<string name="dav_service_refresh_couldnt_refresh">Невозможно обновить список коллекций</string>
<string name="dav_service_refresh_failed">Сбой при обнаружении службы</string>
<string name="dav_service_refresh_couldnt_refresh">Не удалось обновить список коллекций</string>
<!--AppSettingsActivity-->
<string name="app_settings">Настройки</string>
<string name="app_settings_user_interface">Интерфейс пользователя</string>
<string name="app_settings_reset_hints">Включить подсказки</string>
<string name="app_settings_reset_hints_summary">Включить подсказки, которые были отключены ранее</string>
<string name="app_settings_reset_hints_success">Все подсказки будут показаны снова</string>
<string name="app_settings_connection">Соединение</string>
<string name="app_settings_connection">Подключение</string>
<string name="app_settings_override_proxy">Переопределить настройки прокси-сервера</string>
<string name="app_settings_override_proxy_on">Использовать пользовательские настройки прокси-сервера</string>
<string name="app_settings_override_proxy_off">Использовать системные настройки прокси-сервера</string>
@@ -63,7 +75,7 @@
<string name="app_settings_distrust_system_certs_off">Доверять системным и добавленным пользователем CA (рекомендуется)</string>
<string name="app_settings_reset_certificates">Сброс (не)доверенных сертификатов</string>
<string name="app_settings_reset_certificates_summary">Отменить доверие ко всем пользовательским сертификатам</string>
<string name="app_settings_reset_certificates_success">Все пользовательские сертификаты были очищены</string>
<string name="app_settings_reset_certificates_success">Все пользовательские сертификаты были удалены</string>
<string name="app_settings_debug">Отладка</string>
<string name="app_settings_log_to_external_storage">Сохранять лог во внешний файл</string>
<string name="app_settings_log_to_external_storage_on">Сохранение логов во внешнем хранилище (если доступно)</string>
@@ -75,77 +87,111 @@
<string name="account_synchronizing_now">Синхронизация</string>
<string name="account_settings">Настройки аккаунта</string>
<string name="account_rename">Переименовать аккаунт</string>
<string name="account_rename_new_name">Несохранённые локальные данные могут быть потеряны. Требуется выполнить синхронизацию после переименования. Новое имя аккаунта:</string>
<string name="account_rename_new_name">Несохраненные локальные данные могут быть потеряны. Необходима повторная синхронизация после переименования. Новое имя аккаунта:</string>
<string name="account_rename_rename">Переименовать</string>
<string name="account_delete">Удалить аккаунт</string>
<string name="account_delete_confirmation_title">Вы действительно хотите удалить аккаунт?</string>
<string name="account_delete_confirmation_text">Все локальные копии контактов, календарей и задач будут удалены.</string>
<string name="account_delete_confirmation_text">Все локальные копии адресных книг, календарей и задач будут удалены.</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">WebСal</string>
<string name="account_synchronize_this_collection">синхронизировать эту коллекцию</string>
<string name="account_read_only">только для чтения</string>
<string name="account_calendar">календарь</string>
<string name="account_task_list">список задач</string>
<string name="account_refresh_address_book_list">Обновить список адресных книг</string>
<string name="account_create_new_address_book">Создать новую адресную книгу</string>
<string name="account_refresh_calendar_list">Обновить календарь</string>
<string name="account_refresh_calendar_list">Обновить список календарей</string>
<string name="account_create_new_calendar">Создать новый календарь</string>
<string name="account_no_webcal_handler_found">Не найдено приложение, поддерживающее WebCal</string>
<string name="account_install_icsdroid">Установить ICSdroid</string>
<string name="account_read_only_address_book_selected">Адресная книга только для чтения локальные изменения будут отменены!</string>
<!--PermissionsActivity-->
<string name="permissions_title">Разрешения DAVdroid</string>
<string name="permissions_calendar">Разрешения для календаря</string>
<string name="permissions_calendar_details">Для синхронизации CalDAV событий с Вашими локальными календарями DAVdroid должен иметь доступ к Вашим календарям.</string>
<string name="permissions_calendar_request">Запрос разрешений для календаря</string>
<string name="permissions_calendar_details">Для синхронизации событий CalDAV с локальными календарями DAVdroid необходимо получить доступ к календарям.</string>
<string name="permissions_calendar_request">Запросить разрешения календаря</string>
<string name="permissions_contacts">Разрешения для контактов</string>
<string name="permissions_contacts_details">Для синхронизации CardDAV адресных книг с Вашими локальными контактами DAVdroid должен иметь доступ к Вашим контактам.</string>
<string name="permissions_contacts_request">Запрос разрешений для контактов</string>
<string name="permissions_contacts_details">Для синхронизации CardDAV адресных книг с локальными контактами DAVdroid необходимо получить доступ к контактам.</string>
<string name="permissions_contacts_request">Запросить разрешения для контактов</string>
<string name="permissions_opentasks">Разрешения OpenTasks</string>
<string name="permissions_opentasks_details">Для синхронизации CalDAV задач с Вашими локальными списками задач DAVdroid должен иметь доступ к OpenTasks.</string>
<string name="permissions_opentasks_request">Запрос разрешений для OpenTasks</string>
<string name="permissions_opentasks_details">Для синхронизации задач CalDAV с локальными списками задач DAVdroid необходимо получить доступ к OpenTasks.</string>
<string name="permissions_opentasks_request">Запросить разрешения для OpenTasks</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">Добавить аккаунт</string>
<string name="login_type_email">Вход по адресу электронной почты</string>
<string name="login_type_email">Вход с адресом электронной почты</string>
<string name="login_email_address">Адрес электронной почты</string>
<string name="login_email_address_error">Требуется действующий адрес электронной почты</string>
<string name="login_password">Пароль</string>
<string name="login_password_required">Требуется пароль</string>
<string name="login_type_url">Вход через URL и имя пользователя</string>
<string name="login_type_url">Вход с URL и именем пользователя</string>
<string name="login_url_must_be_http_or_https">URL должен начинаться с http(s)://</string>
<string name="login_url_must_be_https">URL-адрес должен начинаться с https://</string>
<string name="login_url_host_name_required">Требуется имя хоста</string>
<string name="login_user_name">Имя</string>
<string name="login_user_name_required">Требуется имя</string>
<string name="login_user_name">Имя пользователя</string>
<string name="login_user_name_required">Требуется Имя пользователя</string>
<string name="login_base_url">Базовый URL</string>
<string name="login_login">Логин</string>
<string name="login_type_url_certificate">Войти с URL-адресом и сертификатом клиента</string>
<string name="login_select_certificate">Выберите сертификат</string>
<string name="login_login">Вход</string>
<string name="login_back">Назад</string>
<string name="login_create_account">Создать аккаунт</string>
<string name="login_account_name">Имя аккаунта</string>
<string name="login_account_name_info">Используйте Ваш адрес адрес электронной почты в качестве имени аккаунта, так как Android будет использовать имя аккаунта в поле ORGANIZER для событий, которые Вы создаёте. Вы не можете иметь два аккаунта с одинаковыми именами.</string>
<string name="login_account_name_info">Используйте ваш адрес адрес электронной почты в качестве имени аккаунта, поскольку Android будет использовать имя аккаунта в поле ORGANIZER для событий, которые вы создаете. У вас не может быть двух аккаунтов с тем же именем.</string>
<string name="login_account_contact_group_method">Метод группировки контактов:</string>
<string name="login_account_name_required">Требуется имя аккаунта</string>
<string name="login_account_not_created">Аккаунт не может быть создан</string>
<string name="login_configuration_detection">Обнаружение конфигурации</string>
<string name="login_querying_server">Пожалуйста, подождите, выполняется запрос к серверу...</string>
<string name="login_no_caldav_carddav">Не найдены CalDAV или CardDAV сервисы.</string>
<string name="login_querying_server">Ожидайте, выполняется запрос к серверу</string>
<string name="login_no_caldav_carddav">Не удалось найти службу CalDAV или CardDAV.</string>
<string name="login_view_logs">Просмотр логов</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Настройки: %s</string>
<string name="settings_authentication">Аутентификация</string>
<string name="settings_username">Имя</string>
<string name="settings_username">Имя пользователя</string>
<string name="settings_enter_username">Введите имя пользователя:</string>
<string name="settings_password">Пароль</string>
<string name="settings_password_summary">Обновить пароль в соответствии с вашим сервером.</string>
<string name="settings_enter_password">Введите свой пароль:</string>
<string name="settings_certificate_alias">Псевдоним сертификата клиента</string>
<string name="settings_sync">Синхронизация</string>
<string name="settings_sync_interval_contacts">Интервал синхронизации контактов</string>
<string name="settings_sync_summary_manually">Вручную</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Каждые %d минут и немедленно при локальных изменениях</string>
<string name="settings_sync_interval_calendars">Интервал синхронизации календарей</string>
<string name="settings_sync_interval_tasks">Интервал синхронизации задач</string>
<string-array name="settings_sync_interval_names">
<item>Только вручную</item>
<item>Каждые 15 минут</item>
<item>Каждые 30 минут</item>
<item>Каждый час</item>
<item>Каждые 2 часа</item>
<item>Каждые 4 часа</item>
<item>Один раз в день</item>
</string-array>
<string name="settings_sync_wifi_only">Синхронизировать только через WiFi</string>
<string name="settings_sync_wifi_only_on">Разрешить синхронизацию только через WiFi</string>
<string name="settings_sync_wifi_only_off">Не учитывать тип соединения</string>
<string name="settings_sync_wifi_only_ssids">Ограничение WiFi SSID</string>
<string name="settings_sync_wifi_only_ssids_on">Будет синхронизироваться только %s</string>
<string name="settings_sync_wifi_only_ssids_off">Все соединения WiFi будут использоваться</string>
<string name="settings_sync_wifi_only_ssids_message">Названия (SSID), разделенные запятыми разрешенных сетей Wi-Fi (оставьте пустым для всех)</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Метод группировки контактов</string>
<string-array name="settings_contact_group_method_entries">
<item>Группы как отдельные vCards</item>
<item>Группы как категории внутри контакта</item>
<string-array name="settings_contact_group_method_values">
<item>GROUP_VCARDS</item>
<item>CATEGORIES</item>
</string-array>
<string-array name="settings_contact_group_method_entries">
<item>Группы являются отдельными vCards</item>
<item>Группы относятся к категориям контактов</item>
</string-array>
<string name="settings_contact_group_method_change">Изменить метод группировки</string>
<string name="settings_contact_group_method_change_reload_contacts">Вам потребуется перезагрузить все контакты. Несохраненные локальные изменения будут отброшены.</string>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Интервал синхронизации</string>
<string name="settings_sync_time_range_past_none">Все события будут синхронизированы</string>
<string name="settings_sync_time_range_past">Ограничение по времени прошедшего события</string>
<string name="settings_sync_time_range_past_none">Все события будут синхронизироваться</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">События старше одного дня будут игнорироваться</item>
<item quantity="few">События старше %d дней будут игнорироваться</item>
@@ -154,60 +200,74 @@
</plurals>
<string name="settings_sync_time_range_past_message">События старше указанного количества дней будут игнорироваться (может быть 0). Оставьте пустым для синхронизации всех событий.</string>
<string name="settings_manage_calendar_colors">Управление цветами календаря</string>
<string name="settings_manage_calendar_colors_on">Цвета календаря управляются DAVdroid</string>
<string name="settings_manage_calendar_colors_off">Цвета календаря не управляются DAVdroid</string>
<string name="settings_manage_calendar_colors_on">Цвета календаря устанавливаются DAVdroid</string>
<string name="settings_manage_calendar_colors_off">Цвета календаря не устанавливаются DAVdroid</string>
<string name="settings_event_colors">Поддержка цвета событий</string>
<string name="settings_event_colors_on">Синхронизация цветов событий</string>
<string name="settings_event_colors_off">Не синхронизировать цвета событий</string>
<string name="settings_event_colors_off_confirm">Отключение цветов событий может удалить уже синхронизированные цвета событий.</string>
<!--collection management-->
<string name="create_addressbook">Создать адресную книгу</string>
<string name="create_addressbook_display_name_hint">Моя Адресная книга</string>
<string name="create_calendar">Создать CalDAV коллекцию</string>
<string name="create_calendar_display_name_hint">Мой Календарь</string>
<string name="create_addressbook_display_name_hint">Моя адресная книга</string>
<string name="create_calendar">Создать коллекцию CalDAV</string>
<string name="create_calendar_display_name_hint">Мой календарь</string>
<string name="create_calendar_time_zone">Часовой пояс:</string>
<string name="create_calendar_type">Тип коллекции:</string>
<string name="create_calendar_type_only_events">Календарь (только события)</string>
<string name="create_calendar_type_only_tasks">Список задач (только задачи)</string>
<string name="create_calendar_type_events_and_tasks">Совмещённый (события и задачи)</string>
<string name="create_calendar_type_events_and_tasks">Совмещенный (события и задачи)</string>
<string name="create_collection_color">Установить цвет коллекции</string>
<string name="create_collection_creating">Создание коллекции</string>
<string name="create_collection_display_name">Отображаемое имя (название) этой коллекции:</string>
<string name="create_collection_display_name_required">Требуется название</string>
<string name="create_collection_description">Описание (необязательно):</string>
<string name="create_collection_home_set">Главная папка:</string>
<string name="create_collection_home_set">Адрес каталога:</string>
<string name="create_collection_create">Создать</string>
<string name="delete_collection">Удалить коллекцию</string>
<string name="delete_collection_confirm_title">Вы уверены?</string>
<string name="delete_collection_confirm_warning">Коллекция (%s) и все её данные будут удалены с сервера.</string>
<string name="delete_collection_confirm_warning">Эта коллекция (%s) и все ее данные будут удалены с сервера.</string>
<string name="delete_collection_deleting_collection">Удаление коллекции</string>
<string name="collection_force_read_only">Принудительно только для чтения</string>
<!--ExceptionInfoFragment-->
<string name="exception">Произошла ошибка.</string>
<string name="exception_httpexception">Произошла ошибка HTTP</string>
<string name="exception_ioexception">Произошла ошибка ввода/вывода.</string>
<string name="exception_show_details">Подробнее</string>
<string name="exception_show_details">Показать детали</string>
<!--sync adapters and DebugInfoActivity-->
<string name="debug_info_title">Отладочная информация</string>
<string name="sync_contacts_read_only_address_book">Адресная книга только для чтения</string>
<plurals name="sync_contacts_local_contact_changes_discarded">
<item quantity="one">Локальное изменение контакта отменено</item>
<item quantity="few">%d локальных изменений контакта отменены</item>
<item quantity="many">%d локальных изменений контакта отменены</item>
<item quantity="other">%d локальных изменений контакта отменены</item>
</plurals>
<string name="sync_error_permissions">Разрешения DAVdroid</string>
<string name="sync_error_permissions_text">Требуются дополнительные разрешения</string>
<string name="sync_error_calendar">Ошибка при синхронизации Календаря (%s)</string>
<string name="sync_error_contacts">Ошибка при синхронизации Контактов (%s)</string>
<string name="sync_error_tasks">Ошибка при синхронизации Задачи (%s)</string>
<string name="sync_error_opentasks_too_old">OpenTasks устарел</string>
<string name="sync_error_opentasks_required_version">Требуется версия: %1$s (текущая %2$s)</string>
<string name="sync_error_calendar">Ошибка при синхронизации календаря (%s)</string>
<string name="sync_error_contacts">Ошибка при синхронизации адресной книги (%s)</string>
<string name="sync_error_tasks">Ошибка при синхронизации задач (%s)</string>
<string name="sync_error">Ошибка %s</string>
<string name="sync_error_http_dav">Ошибка сервера %s</string>
<string name="sync_error_local_storage">Ошибка базы данных %s</string>
<string-array name="sync_error_phases">
<item>подготовка синхронизации</item>
<item>запрос поддерживаемых возможностей</item>
<item>обработка локально удалённых записей</item>
<item>подготовка созданных/изменённых записей</item>
<item>загрузка созданных/изменённых записей</item>
<item>запрос возможностей сервера</item>
<item>локальная обработка удаленных данных</item>
<item>подготовка созданных/измененных записей</item>
<item>загрузка созданных/измененных записей</item>
<item>проверка статуса синхронизации</item>
<item>просмотр локальных записей</item>
<item>просмотр серверных записей</item>
<item>сравнение локальных/серверных записей</item>
<item>загрузка серверных записей</item>
<item>пост-обработка</item>
<item>просмотр удаленных записей</item>
<item>сравнение локальных/удаленных записей</item>
<item>загрузка удаленных записей</item>
<item>постобработка</item>
<item>сохранение статуса синхронизации</item>
</string-array>
<string name="sync_error_unauthorized">Имя пользователя/пароль неверные</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: Безопасность подключения</string>
<string name="certificate_notification_connection_security">DAVdroid: безопасность подключения</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid обнаружил неизвестный сертификат. Вы хотите доверять ему?</string>
</resources>

View File

@@ -42,6 +42,10 @@
android:id="@+id/nav_website"
android:icon="@drawable/ic_home_dark"
android:title="@string/navigation_drawer_website"/>
<item
android:id="@+id/nav_manual"
android:icon="@drawable/ic_info_dark"
android:title="@string/navigation_drawer_manual"/>
<item
android:id="@+id/nav_faq"
android:icon="@drawable/ic_help_dark"

View File

@@ -55,12 +55,6 @@
<service android:name=".DavService"/>
<service android:name=".settings.Settings"/>
<receiver android:name=".AccountsChangedReceiver">
<intent-filter>
<action android:name="android.accounts.LOGIN_ACCOUNTS_CHANGED"/>
</intent-filter>
</receiver>
<activity
android:name=".ui.AccountsActivity"
android:label="@string/app_name"

View File

@@ -9,10 +9,7 @@ package at.bitfire.davdroid
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.*
import android.os.Build
import android.os.Bundle
import android.os.Parcel
@@ -21,6 +18,7 @@ import android.provider.CalendarContract
import android.provider.ContactsContract
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.ServiceDB.*
import at.bitfire.davdroid.model.ServiceDB.Collections
@@ -31,10 +29,12 @@ import at.bitfire.davdroid.settings.ISettings
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.ContactsStorageException
import at.bitfire.vcard4android.GroupMethod
import okhttp3.HttpUrl
import org.apache.commons.lang3.StringUtils
import org.dmfs.tasks.contract.TaskContract
import java.util.*
import java.util.logging.Level
@@ -46,10 +46,11 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
companion object {
val CURRENT_VERSION = 7
val CURRENT_VERSION = 8
val KEY_SETTINGS_VERSION = "version"
val KEY_USERNAME = "user_name"
val KEY_CERTIFICATE_ALIAS = "certificate_alias"
val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false)
val KEY_WIFI_ONLY_SSIDS = "wifi_only_ssids" // restrict sync to specific WiFi SSIDs
@@ -81,10 +82,17 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
@JvmField
val SYNC_INTERVAL_MANUALLY = -1L
fun initialUserData(userName: String): Bundle {
fun initialUserData(credentials: Credentials): Bundle {
val bundle = Bundle(2)
bundle.putString(KEY_SETTINGS_VERSION, CURRENT_VERSION.toString())
bundle.putString(KEY_USERNAME, userName)
when (credentials.type) {
Credentials.Type.UsernamePassword ->
bundle.putString(KEY_USERNAME, credentials.userName)
Credentials.Type.ClientCertificate ->
bundle.putString(KEY_CERTIFICATE_ALIAS, credentials.certificateAlias)
}
return bundle
}
@@ -111,11 +119,17 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
// authentication settings
fun username(): String? = accountManager.getUserData(account, KEY_USERNAME)
fun username(userName: String) = accountManager.setUserData(account, KEY_USERNAME, userName)
fun credentials() = Credentials(
accountManager.getUserData(account, KEY_USERNAME),
accountManager.getPassword(account),
accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS)
)
fun password(): String? = accountManager.getPassword(account)
fun password(password: String) = accountManager.setPassword(account, password)
fun credentials(credentials: Credentials) {
accountManager.setUserData(account, KEY_USERNAME, credentials.userName)
accountManager.setPassword(account, credentials.password)
accountManager.setUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias)
}
// sync. settings
@@ -210,6 +224,8 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
try {
val updateProc = this::class.java.getDeclaredMethod("update_${fromVersion}_$toVersion")
updateProc.invoke(this)
Logger.log.info("Account version update successful")
accountManager.setUserData(account, KEY_SETTINGS_VERSION, toVersion.toString())
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't update account settings", e)
@@ -217,6 +233,32 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
}
}
@Suppress("unused")
private fun update_7_8() {
TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)?.let { provider ->
// ETag is now in sync_version instead of sync1
// UID is now in _uid instead of sync2
val cursor = provider.client.query(TaskProvider.syncAdapterUri(provider.tasksUri(), account),
arrayOf(TaskContract.Tasks._ID, TaskContract.Tasks.SYNC1, TaskContract.Tasks.SYNC2),
"${TaskContract.Tasks.ACCOUNT_TYPE}=? AND ${TaskContract.Tasks.ACCOUNT_NAME}=?",
arrayOf(account.type, account.name), null)
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
val eTag = cursor.getString(1)
val uid = cursor.getString(2)
val values = ContentValues(4)
values.put(TaskContract.Tasks._UID, uid)
values.put(TaskContract.Tasks.SYNC_VERSION, eTag)
values.putNull(TaskContract.Tasks.SYNC1)
values.putNull(TaskContract.Tasks.SYNC2)
Logger.log.log(Level.FINER, "Updating task $id", values)
provider.client.update(
TaskProvider.syncAdapterUri(ContentUris.withAppendedId(provider.tasksUri(), id), account),
values, null, null)
}
}
}
@Suppress("unused")
private fun update_6_7() {
// add calendar colors
@@ -391,7 +433,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
}
}
AndroidTaskList.acquireTaskProvider(context.contentResolver)?.use { provider ->
AndroidTaskList.acquireTaskProvider(context)?.use { provider ->
try {
val taskLists = AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null)
for (taskList in taskLists)

View File

@@ -1,48 +0,0 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid
import android.accounts.AccountManager
import android.accounts.OnAccountsUpdateListener
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import java.util.*
class AccountsChangedReceiver: BroadcastReceiver() {
companion object {
private val listeners = LinkedList<OnAccountsUpdateListener>()
@JvmStatic
fun registerListener(listener: OnAccountsUpdateListener, callImmediately: Boolean) {
listeners += listener
if (callImmediately)
listener.onAccountsUpdated(null)
}
@JvmStatic
fun unregisterListener(listener: OnAccountsUpdateListener) {
listeners -= listener
}
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION) {
val serviceIntent = Intent(context, DavService::class.java)
serviceIntent.action = DavService.ACTION_ACCOUNTS_UPDATED
context.startService(serviceIntent)
for (listener in listeners)
listener.onAccountsUpdated(null)
}
}
}

View File

@@ -5,7 +5,7 @@
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid;
package at.bitfire.davdroid
object Constants {

View File

@@ -15,12 +15,15 @@ import java.net.InetAddress
import java.net.Socket
import java.security.GeneralSecurityException
import java.util.*
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
import javax.net.ssl.*
class SSLSocketFactoryCompat(
/**
* Custom TLS socket factory with support for
* - enabling/disabling algorithms depending on the Android version,
* - client certificate authentication
*/
class CustomTlsSocketFactory(
keyManager: KeyManager?,
trustManager: X509TrustManager
): SSLSocketFactory() {
@@ -54,7 +57,7 @@ class SSLSocketFactoryCompat(
protocols = _protocols.toTypedArray()
/* set up reasonable cipher suites */
val knownCiphers = arrayOf<String>(
val knownCiphers = arrayOf(
// TLS 1.2
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
@@ -105,7 +108,10 @@ class SSLSocketFactoryCompat(
init {
try {
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(trustManager), null)
sslContext.init(
if (keyManager != null) arrayOf(keyManager) else null,
arrayOf(trustManager),
null)
delegate = sslContext.socketFactory
} catch (e: GeneralSecurityException) {
throw IllegalStateException() // system has no TLS

View File

@@ -9,8 +9,6 @@
package at.bitfire.davdroid
import android.accounts.Account
import android.accounts.AccountManager
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.Service
import android.content.ContentValues
@@ -28,11 +26,9 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.ServiceDB.*
import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.vcard4android.ContactsStorageException
import okhttp3.HttpUrl
import org.apache.commons.collections4.iterators.IteratorChain
import org.apache.commons.collections4.iterators.SingletonIterator
@@ -45,7 +41,6 @@ import kotlin.concurrent.thread
class DavService: Service() {
companion object {
@JvmField val ACTION_ACCOUNTS_UPDATED = "accountsUpdated"
@JvmField val ACTION_REFRESH_COLLECTIONS = "refreshCollections"
@JvmField val EXTRA_DAV_SERVICE_ID = "davServiceID"
}
@@ -59,8 +54,6 @@ class DavService: Service() {
val id = intent.getLongExtra(EXTRA_DAV_SERVICE_ID, -1)
when (intent.action) {
ACTION_ACCOUNTS_UPDATED ->
cleanupAccounts()
ACTION_REFRESH_COLLECTIONS ->
if (runningRefresh.add(id)) {
thread { refreshCollections(id) }
@@ -110,42 +103,7 @@ class DavService: Service() {
which actually do the work
*/
@SuppressLint("MissingPermission")
fun cleanupAccounts() {
Logger.log.info("Cleaning up orphaned accounts")
OpenHelper(this).use { dbHelper ->
val db = dbHelper.writableDatabase
val sqlAccountNames = LinkedList<String>()
val accountNames = HashSet<String>()
val accountManager = AccountManager.get(this)
for (account in accountManager.getAccountsByType(getString(R.string.account_type))) {
sqlAccountNames.add(DatabaseUtils.sqlEscapeString(account.name))
accountNames += account.name
}
// delete orphaned address book accounts
accountManager.getAccountsByType(getString(R.string.account_type_address_book))
.map { LocalAddressBook(this, it, null) }
.forEach {
try {
if (!accountNames.contains(it.getMainAccount().name))
it.delete()
} catch(e: ContactsStorageException) {
Logger.log.log(Level.SEVERE, "Couldn't get address book main account", e)
}
}
// delete orphaned services in DB
if (sqlAccountNames.isEmpty())
db.delete(Services._TABLE, null, null)
else
db.delete(Services._TABLE, "${Services.ACCOUNT_NAME} NOT IN (${sqlAccountNames.joinToString(",")})", null)
}
}
fun refreshCollections(service: Long) {
private fun refreshCollections(service: Long) {
OpenHelper(this@DavService).use { dbHelper ->
val db = dbHelper.writableDatabase
@@ -204,13 +162,13 @@ class DavService: Service() {
when (serviceType) {
Services.SERVICE_CARDDAV -> {
dav.propfind(0, AddressbookHomeSet.NAME, GroupMembership.NAME)
for ((resource, addressbookHomeSet) in dav.findProperties(AddressbookHomeSet.NAME) as List<Pair<DavResource, AddressbookHomeSet>>)
for ((resource, addressbookHomeSet) in dav.findProperties(AddressbookHomeSet::class.java))
for (href in addressbookHomeSet.hrefs)
resource.location.resolve(href)?.let { homeSets += UrlUtils.withTrailingSlash(it) }
}
Services.SERVICE_CALDAV -> {
dav.propfind(0, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME)
for ((resource, calendarHomeSet) in dav.findProperties(CalendarHomeSet.NAME) as List<Pair<DavResource, CalendarHomeSet>>)
for ((resource, calendarHomeSet) in dav.findProperties(CalendarHomeSet::class.java))
for (href in calendarHomeSet.hrefs)
resource.location.resolve(href)?.let { homeSets.add(UrlUtils.withTrailingSlash(it)) }
}
@@ -255,19 +213,19 @@ class DavService: Service() {
queryHomeSets(principal)
// refresh home sets: calendar-proxy-read/write-for
for ((resource, proxyRead) in principal.findProperties(CalendarProxyReadFor.NAME) as List<Pair<DavResource, CalendarProxyReadFor>>)
for ((resource, proxyRead) in principal.findProperties(CalendarProxyReadFor::class.java))
for (href in proxyRead.hrefs) {
Logger.log.fine("Principal is a read-only proxy for $href, checking for home sets")
resource.location.resolve(href)?.let { queryHomeSets(DavResource(httpClient, it)) }
}
for ((resource, proxyWrite) in principal.findProperties(CalendarProxyWriteFor.NAME) as List<Pair<DavResource, CalendarProxyWriteFor>>)
for ((resource, proxyWrite) in principal.findProperties(CalendarProxyWriteFor::class.java))
for (href in proxyWrite.hrefs) {
Logger.log.fine("Principal is a read/write proxy for $href, checking for home sets")
resource.location.resolve(href)?.let { queryHomeSets(DavResource(httpClient, it)) }
}
// refresh home sets: direct group memberships
(principal.properties[GroupMembership.NAME] as GroupMembership?)?.let { groupMembership ->
principal.properties[GroupMembership::class.java]?.let { groupMembership ->
for (href in groupMembership.hrefs) {
Logger.log.fine("Principal is member of group $href, checking for home sets")
principal.location.resolve(href)?.let { url ->

View File

@@ -6,15 +6,20 @@
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid;
package at.bitfire.davdroid
import android.annotation.TargetApi
import android.content.Context
import android.net.ConnectivityManager
import android.os.Build
import at.bitfire.davdroid.log.Logger
import okhttp3.HttpUrl
import org.xbill.DNS.Record
import org.xbill.DNS.SRVRecord
import org.xbill.DNS.TXTRecord
import org.xbill.DNS.*
import java.util.*
/**
* Some WebDAV and related network utility methods
*/
object DavUtils {
@JvmStatic
@@ -32,11 +37,27 @@ object DavUtils {
val segments = LinkedList<String>(httpUrl.pathSegments())
Collections.reverse(segments)
for (segment in segments)
if (segment.isNotEmpty())
return segment
return segments.firstOrNull { it.isNotEmpty() } ?: "/"
}
return "/"
fun prepareLookup(context: Context, lookup: Lookup) {
@TargetApi(Build.VERSION_CODES.O)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
/* Since Android 8, the system properties net.dns1, net.dns2, ... are not available anymore.
The current version of dnsjava relies on these properties to find the default name servers,
so we have to add the servers explicitly (fortunately, there's an Android API to
get the active DNS servers). */
val connectivity = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeLink = connectivity.getLinkProperties(connectivity.activeNetwork)
val simpleResolvers = activeLink.dnsServers.map {
Logger.log.fine("Using DNS server ${it.hostAddress}")
val resolver = SimpleResolver()
resolver.setAddress(it)
resolver
}
val resolver = ExtendedResolver(simpleResolvers.toTypedArray())
lookup.setResolver(resolver)
}
}
fun selectSRVRecord(records: Array<Record>?): SRVRecord? {

View File

@@ -6,14 +6,16 @@
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid;
package at.bitfire.davdroid
import android.content.Context
import android.os.Build
import android.security.KeyChain
import at.bitfire.cert4android.CustomCertManager
import at.bitfire.dav4android.BasicDigestAuthHandler
import at.bitfire.dav4android.UrlUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.settings.ISettings
import okhttp3.Cache
import okhttp3.Interceptor
@@ -25,16 +27,40 @@ import java.io.Closeable
import java.io.File
import java.net.InetSocketAddress
import java.net.Proxy
import java.text.SimpleDateFormat
import java.net.Socket
import java.security.KeyStore
import java.security.Principal
import java.text.DateFormat
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.logging.Level
import javax.net.ssl.KeyManager
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509ExtendedKeyManager
import javax.net.ssl.X509TrustManager
class HttpClient private constructor(
val okHttpClient: OkHttpClient,
private val certManager: CustomCertManager?
): Closeable {
companion object {
/** [OkHttpClient] singleton to build all clients from */
val sharedClient = OkHttpClient.Builder()
// set timeouts
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
// don't allow redirects by default, because it would break PROPFIND handling
.followRedirects(false)
// add User-Agent to every request
.addNetworkInterceptor(UserAgentInterceptor)
.build()!!
}
override fun close() {
certManager?.close()
}
@@ -43,23 +69,14 @@ class HttpClient private constructor(
val context: Context? = null,
val settings: ISettings? = null,
accountSettings: AccountSettings? = null,
logger: java.util.logging.Logger = Logger.log
val logger: java.util.logging.Logger = Logger.log
) {
var certManager: CustomCertManager? = null
private val orig = OkHttpClient.Builder()
private var certManager: CustomCertManager? = null
private var certificateAlias: String? = null
private val orig = sharedClient.newBuilder()
init {
// set timeouts
orig.connectTimeout(30, TimeUnit.SECONDS)
orig.writeTimeout(30, TimeUnit.SECONDS)
orig.readTimeout(120, TimeUnit.SECONDS)
// don't allow redirects by default, because it would break PROPFIND handling
orig.followRedirects(false)
// add User-Agent to every request
orig.addNetworkInterceptor(UserAgentInterceptor)
// add cookie store for non-persistent cookies (some services like Horde use cookies for session tracking)
orig.cookieJar(MemoryCookieStore())
@@ -95,17 +112,14 @@ class HttpClient private constructor(
// use account settings for authentication
accountSettings?.let {
val userName = accountSettings.username()
val password = accountSettings.password()
if (userName != null && password != null)
addAuthentication(null, userName, password)
addAuthentication(null, it.credentials())
}
}
}
}
constructor(context: Context, host: String?, username: String, password: String): this(context) {
addAuthentication(host, username, password)
constructor(context: Context, host: String?, credentials: Credentials): this(context) {
addAuthentication(host, credentials)
}
fun withDiskCache(): Builder {
@@ -129,8 +143,6 @@ class HttpClient private constructor(
fun customCertManager(manager: CustomCertManager) {
certManager = manager
orig.sslSocketFactory(SSLSocketFactoryCompat(manager), manager)
orig.hostnameVerifier(manager.hostnameVerifier(OkHostnameVerifier.INSTANCE))
}
fun setForeground(foreground: Boolean): Builder {
@@ -138,14 +150,70 @@ class HttpClient private constructor(
return this
}
fun addAuthentication(host: String?, username: String, password: String): Builder {
val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), username, password)
orig .addNetworkInterceptor(authHandler)
.authenticator(authHandler)
fun addAuthentication(host: String?, credentials: Credentials): Builder {
when (credentials.type) {
Credentials.Type.UsernamePassword -> {
val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), credentials.userName!!, credentials.password!!)
orig .addNetworkInterceptor(authHandler)
.authenticator(authHandler)
}
Credentials.Type.ClientCertificate -> {
certificateAlias = credentials.certificateAlias
}
}
return this
}
fun build() = HttpClient(orig.build(), certManager)
fun build(): HttpClient {
val trustManager = certManager ?: {
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
factory.init(null as KeyStore?)
factory.trustManagers.first() as X509TrustManager
}()
val hostnameVerifier = certManager?.hostnameVerifier(OkHostnameVerifier.INSTANCE)
?: OkHostnameVerifier.INSTANCE
var keyManager: KeyManager? = null
try {
certificateAlias?.let { alias ->
// get client certificate and private key
val certs = KeyChain.getCertificateChain(context, alias) ?: return@let
val key = KeyChain.getPrivateKey(context, alias) ?: return@let
logger.fine("Using client certificate $alias for authentication (chain length: ${certs.size})")
// create Android KeyStore (performs key operations without revealing secret data to DAVdroid)
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
// create KeyManager
keyManager = object: X509ExtendedKeyManager() {
override fun getServerAliases(p0: String?, p1: Array<out Principal>?): Array<String>? = null
override fun chooseServerAlias(p0: String?, p1: Array<out Principal>?, p2: Socket?) = null
override fun getClientAliases(p0: String?, p1: Array<out Principal>?) =
arrayOf(alias)
override fun chooseClientAlias(p0: Array<out String>?, p1: Array<out Principal>?, p2: Socket?) =
alias
override fun getCertificateChain(forAlias: String?) =
certs.takeIf { forAlias == alias }
override fun getPrivateKey(forAlias: String?) =
key.takeIf { forAlias == alias }
}
}
} catch (e: Exception) {
logger.log(Level.SEVERE, "Couldn't set up client certificate authentication", e)
}
orig.sslSocketFactory(CustomTlsSocketFactory(keyManager, trustManager), trustManager)
orig.hostnameVerifier(hostnameVerifier)
return HttpClient(orig.build(), certManager)
}
}
@@ -156,7 +224,7 @@ class HttpClient private constructor(
App.FLAVOR_SOLDUPE -> "Soldupe Sync"
else -> "DAVdroid"
}
private val userAgentDate = SimpleDateFormat("yyyy/MM/dd", Locale.US).format(Date(BuildConfig.buildTime))
private val userAgentDate = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US).format(Date(BuildConfig.buildTime))
private val userAgent = "$productName/${BuildConfig.VERSION_NAME} ($userAgentDate; dav4android; okhttp3) Android/${Build.VERSION.RELEASE}"
override fun intercept(chain: Interceptor.Chain): Response {

View File

@@ -42,7 +42,7 @@ class PackageChangedReceiver: BroadcastReceiver() {
if (ContentResolver.getIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority) <= 0) {
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 1)
ContentResolver.setSyncAutomatically(account, TaskProvider.ProviderName.OpenTasks.authority, true)
ContentResolver.addPeriodicSync(account, TaskProvider.ProviderName.OpenTasks.authority, Bundle(), Constants.DEFAULT_SYNC_INTERVAL.toLong())
ContentResolver.addPeriodicSync(account, TaskProvider.ProviderName.OpenTasks.authority, Bundle(), Constants.DEFAULT_SYNC_INTERVAL)
}
} else
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0)

View File

@@ -8,8 +8,8 @@
package at.bitfire.davdroid.log
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Handler
import java.util.logging.LogRecord
class StringHandler: Handler() {

View File

@@ -62,7 +62,7 @@ data class CollectionInfo @JvmOverloads constructor(
constructor(dav: DavResource): this(dav.location.toString()) {
(dav.properties[ResourceType.NAME] as ResourceType?)?.let { type ->
dav.properties[ResourceType::class.java]?.let { type ->
when {
type.types.contains(ResourceType.ADDRESSBOOK) -> this.type = Type.ADDRESS_BOOK
type.types.contains(ResourceType.CALENDAR) -> this.type = Type.CALENDAR
@@ -70,33 +70,33 @@ data class CollectionInfo @JvmOverloads constructor(
}
}
(dav.properties[CurrentUserPrivilegeSet.NAME] as CurrentUserPrivilegeSet?)?.let { privilegeSet ->
dav.properties[CurrentUserPrivilegeSet::class.java]?.let { privilegeSet ->
readOnly = !privilegeSet.mayWriteContent
}
(dav.properties[DisplayName.NAME] as DisplayName?)?.let {
dav.properties[DisplayName::class.java]?.let {
if (!it.displayName.isNullOrEmpty())
displayName = it.displayName
}
when (type) {
Type.ADDRESS_BOOK -> {
(dav.properties[AddressbookDescription.NAME] as AddressbookDescription?)?.let { description = it.description }
dav.properties[AddressbookDescription::class.java]?.let { description = it.description }
}
Type.CALENDAR, Type.WEBCAL -> {
(dav.properties[CalendarDescription.NAME] as CalendarDescription?)?.let { description = it.description }
(dav.properties[CalendarColor.NAME] as CalendarColor?)?.let { color = it.color }
(dav.properties[CalendarTimezone.NAME] as CalendarTimezone?)?.let { timeZone = it.vTimeZone }
dav.properties[CalendarDescription::class.java]?.let { description = it.description }
dav.properties[CalendarColor::class.java]?.let { color = it.color }
dav.properties[CalendarTimezone::class.java]?.let { timeZone = it.vTimeZone }
if (type == Type.CALENDAR) {
supportsVEVENT = true
supportsVTODO = true
(dav.properties[SupportedCalendarComponentSet.NAME] as SupportedCalendarComponentSet?)?.let {
dav.properties[SupportedCalendarComponentSet::class.java]?.let {
supportsVEVENT = it.supportsEvents
supportsVTODO = it.supportsTasks
}
} else { // Type.WEBCAL
(dav.properties[Source.NAME] as Source?)?.let { source = it.hrefs.firstOrNull() }
dav.properties[Source::class.java]?.let { source = it.hrefs.firstOrNull() }
supportsVEVENT = true
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.model
import java.io.Serializable
class Credentials @JvmOverloads constructor(
@JvmField val userName: String? = null,
@JvmField val password: String? = null,
@JvmField val certificateAlias: String? = null
): Serializable {
enum class Type {
UsernamePassword,
ClientCertificate
}
val type: Type
init {
type = when {
!certificateAlias.isNullOrEmpty() ->
Type.ClientCertificate
!userName.isNullOrEmpty() && !password.isNullOrEmpty() ->
Type.UsernamePassword
else ->
throw IllegalArgumentException("Either username/password or certificate alias must be set")
}
}
override fun toString(): String {
return "Credentials(type=$type, userName=$userName, certificateAlias=$certificateAlias)"
}
}

View File

@@ -127,7 +127,7 @@ class ServiceDB {
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
for (upgradeFrom in oldVersion until newVersion) {
val upgradeTo = oldVersion + 1
val upgradeTo = upgradeFrom + 1
Logger.log.info("Upgrading database from version $upgradeFrom to $upgradeTo")
try {
val upgradeProc = this::class.java.getDeclaredMethod("upgrade_${upgradeFrom}_$upgradeTo", SQLiteDatabase::class.java)
@@ -193,7 +193,7 @@ class ServiceDB {
// print columns
val cols = cursor.columnCount
sb.append("\t| ")
for (i in 0 .. cols-1)
for (i in 0 until cols)
sb .append(" ")
.append(cursor.getColumnName(i))
.append(" |")
@@ -202,7 +202,7 @@ class ServiceDB {
// print rows
while (cursor.moveToNext()) {
sb.append("\t| ")
for (i in 0 .. cols-1) {
for (i in 0 until cols) {
sb.append(" ")
try {
val value = cursor.getString(i)

View File

@@ -6,9 +6,9 @@
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.model;
package at.bitfire.davdroid.model
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.RawContacts
object UnknownProperties {

View File

@@ -9,6 +9,7 @@ package at.bitfire.davdroid.resource
import android.accounts.Account
import android.accounts.AccountManager
import android.annotation.TargetApi
import android.content.*
import android.os.Build
import android.os.Bundle
@@ -28,6 +29,12 @@ import java.io.FileNotFoundException
import java.util.*
import java.util.logging.Level
/**
* A local address book. Requires an own Android account, because Android manages contacts per
* account and there is no such thing as "address books". So, DAVdroid creates a "DAVdroid
* address book" account for every CardDAV address book. These accounts are bound to a
* DAVdroid main account.
*/
class LocalAddressBook(
private val context: Context,
account: Account,
@@ -53,6 +60,13 @@ class LocalAddressBook(
val addressBook = LocalAddressBook(context, account, provider)
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
// set up Contacts Provider Settings
val values = ContentValues(2)
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
addressBook.updateSettings(values)
return addressBook
}
@@ -109,6 +123,8 @@ class LocalAddressBook(
@Throws(ContactsStorageException::class)
fun update(info: CollectionInfo) {
val newAccountName = accountName(getMainAccount(), info)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
if (account.name != newAccountName && Build.VERSION.SDK_INT >= 21) {
val accountManager = AccountManager.get(context)
val future = accountManager.renameAccount(account, newAccountName, {
@@ -308,15 +324,6 @@ class LocalAddressBook(
}
}
@Throws(ContactsStorageException::class)
fun removeGroups() {
try {
provider!!.delete(syncAdapterURI(Groups.CONTENT_URI), null, null)
} catch(e: RemoteException) {
throw ContactsStorageException("Couldn't remove all groups", e)
}
}
// SETTINGS

View File

@@ -56,9 +56,7 @@ class LocalCalendar private constructor(
// flag as visible & synchronizable at creation, might be changed by user at any time
values.put(Calendars.VISIBLE, 1)
values.put(Calendars.SYNC_EVENTS, 1)
val uri = create(account, provider, values)
return uri
return create(account, provider, values)
}
private fun valuesFromCollectionInfo(info: CollectionInfo, withColor: Boolean): ContentValues {

View File

@@ -12,7 +12,7 @@ import android.content.ContentProviderOperation
import android.content.ContentValues
import android.provider.CalendarContract.Events
import at.bitfire.ical4android.*
import org.dmfs.provider.tasks.TaskContract.Tasks
import org.dmfs.tasks.contract.TaskContract.Tasks
import java.io.FileNotFoundException
import java.text.ParseException
import java.util.*
@@ -20,8 +20,7 @@ import java.util.*
class LocalTask: AndroidTask, LocalResource {
companion object {
val COLUMN_ETAG = Tasks.SYNC1
val COLUMN_UID = Tasks.SYNC2
val COLUMN_ETAG = Tasks.SYNC_VERSION
val COLUMN_SEQUENCE = Tasks.SYNC3
}
@@ -51,7 +50,6 @@ class LocalTask: AndroidTask, LocalResource {
eTag = values.getAsString(COLUMN_ETAG)
val task = requireNotNull(task)
task.uid = values.getAsString(COLUMN_UID)
task.sequence = values.getAsInteger(COLUMN_SEQUENCE)
}
@@ -61,7 +59,6 @@ class LocalTask: AndroidTask, LocalResource {
val task = requireNotNull(task)
builder .withValue(Tasks._SYNC_ID, fileName)
.withValue(COLUMN_UID, task.uid)
.withValue(COLUMN_SEQUENCE, task.sequence)
.withValue(COLUMN_ETAG, eTag)
}
@@ -77,7 +74,7 @@ class LocalTask: AndroidTask, LocalResource {
val values = ContentValues(2)
values.put(Tasks._SYNC_ID, newFileName)
values.put(COLUMN_UID, uid)
values.put(Tasks._UID, uid)
taskList.provider.client.update(taskSyncURI(), values, null, null)
fileName = newFileName

View File

@@ -21,8 +21,8 @@ import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.AndroidTaskListFactory
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.TaskProvider
import org.dmfs.provider.tasks.TaskContract.TaskLists
import org.dmfs.provider.tasks.TaskContract.Tasks
import org.dmfs.tasks.contract.TaskContract.TaskLists
import org.dmfs.tasks.contract.TaskContract.Tasks
import java.io.FileNotFoundException
class LocalTaskList private constructor(
@@ -48,7 +48,7 @@ class LocalTaskList private constructor(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
return context.packageManager.resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null
else {
val provider = TaskProvider.acquire(context.contentResolver, TaskProvider.ProviderName.OpenTasks)
val provider = TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)
provider?.use { return true }
return false
}
@@ -102,7 +102,7 @@ class LocalTaskList private constructor(
@Throws(CalendarStorageException::class)
fun update(info: CollectionInfo, updateColor: Boolean) {
update(valuesFromCollectionInfo(info, updateColor));
update(valuesFromCollectionInfo(info, updateColor))
}

View File

@@ -14,18 +14,18 @@ open class DefaultsProvider(
private val allowOverride: Boolean = true
): Provider {
open val booleanDefaults = mapOf<String, Boolean>(
open val booleanDefaults = mapOf(
Pair(App.DISTRUST_SYSTEM_CERTIFICATES, false),
Pair(App.OVERRIDE_PROXY, false)
)
open val intDefaults = mapOf<String, Int>(
open val intDefaults = mapOf(
Pair(App.OVERRIDE_PROXY_PORT, App.OVERRIDE_PROXY_PORT_DEFAULT)
)
open val longDefaults = mapOf<String, Long>()
open val stringDefaults = mapOf<String, String>(
open val stringDefaults = mapOf(
Pair(App.OVERRIDE_PROXY_HOST, App.OVERRIDE_PROXY_HOST_DEFAULT)
)

View File

@@ -7,28 +7,93 @@
*/
package at.bitfire.davdroid.syncadapter
import android.accounts.AbstractAccountAuthenticator
import android.accounts.Account
import android.accounts.AccountAuthenticatorResponse
import android.accounts.AccountManager
import android.accounts.*
import android.app.Service
import android.content.Context
import android.content.Intent
import android.database.DatabaseUtils
import android.os.Bundle
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.ui.setup.LoginActivity
import at.bitfire.vcard4android.ContactsStorageException
import java.util.*
import java.util.logging.Level
class AccountAuthenticatorService: Service() {
/**
* Account authenticator for the main DAVdroid account type.
*
* Gets started when a DAVdroid account is removed, too, so it also watches for account removals
* and contains the corresponding cleanup code.
*/
class AccountAuthenticatorService: Service(), OnAccountsUpdateListener {
companion object {
fun cleanupAccounts(context: Context) {
Logger.log.info("Cleaning up orphaned accounts")
ServiceDB.OpenHelper(context).use { dbHelper ->
val db = dbHelper.writableDatabase
val sqlAccountNames = LinkedList<String>()
val accountNames = HashSet<String>()
val accountManager = AccountManager.get(context)
for (account in accountManager.getAccountsByType(context.getString(R.string.account_type))) {
sqlAccountNames.add(DatabaseUtils.sqlEscapeString(account.name))
accountNames += account.name
}
// delete orphaned address book accounts
accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
.map { LocalAddressBook(context, it, null) }
.forEach {
try {
if (!accountNames.contains(it.getMainAccount().name))
it.delete()
} catch(e: ContactsStorageException) {
Logger.log.log(Level.SEVERE, "Couldn't get address book main account", e)
}
}
// delete orphaned services in DB
if (sqlAccountNames.isEmpty())
db.delete(ServiceDB.Services._TABLE, null, null)
else
db.delete(ServiceDB.Services._TABLE, "${ServiceDB.Services.ACCOUNT_NAME} NOT IN (${sqlAccountNames.joinToString(",")})", null)
}
}
}
private lateinit var accountManager: AccountManager
private lateinit var accountAuthenticator: AccountAuthenticator
override fun onCreate() {
accountManager = AccountManager.get(this)
accountManager.addOnAccountsUpdatedListener(this, null, true)
accountAuthenticator = AccountAuthenticator(this)
}
override fun onDestroy() {
super.onDestroy()
accountManager.removeOnAccountsUpdatedListener(this)
}
override fun onBind(intent: Intent?) =
accountAuthenticator.iBinder.takeIf { intent?.action == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT }
override fun onAccountsUpdated(accounts: Array<out Account>?) {
cleanupAccounts(this)
}
private class AccountAuthenticator(
val context: Context
): AbstractAccountAuthenticator(context) {

View File

@@ -52,7 +52,9 @@ class CalendarSyncManager(
val localCalendar: LocalCalendar
): SyncManager(context, settings, account, accountSettings, extras, authority, syncResult, "calendar/${localCalendar.id}") {
val MAX_MULTIGET = 20
companion object {
private val MAX_MULTIGET = 20
}
init {
localCollection = localCalendar
@@ -123,9 +125,7 @@ class CalendarSyncManager(
// download new/updated iCalendars from server
for (bunch in ListUtils.partition(toDownload.toList(), MAX_MULTIGET)) {
if (Thread.interrupted())
return
abortIfCancelled()
Logger.log.info("Downloading ${bunch.joinToString(", ")}")
if (bunch.size == 1) {
@@ -136,7 +136,7 @@ class CalendarSyncManager(
val body = remote.get("text/calendar")
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
val eTag = remote.properties[GetETag.NAME] as GetETag?
val eTag = remote.properties[GetETag::class.java]
if (eTag == null || eTag.eTag.isNullOrEmpty())
throw DavException("Received CalDAV GET response without ETag for ${remote.location}")
@@ -154,10 +154,10 @@ class CalendarSyncManager(
for (remote in davCollection.members) {
currentDavResource = remote
val eTag = (remote.properties[GetETag.NAME] as GetETag?)?.eTag
val eTag = remote.properties[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val calendarData = remote.properties[CalendarData.NAME] as CalendarData?
val calendarData = remote.properties[CalendarData::class.java]
val iCalendar = calendarData?.iCalendar
?: throw DavException("Received multi-get response without event data")

View File

@@ -13,7 +13,6 @@ import android.content.*
import android.os.Build
import android.os.Bundle
import android.os.RemoteException
import android.provider.ContactsContract
import android.provider.ContactsContract.Groups
import android.support.v4.app.NotificationCompat
import at.bitfire.dav4android.DavAddressBook
@@ -41,39 +40,39 @@ import java.util.*
import java.util.logging.Level
/**
* <p>Synchronization manager for CardDAV collections; handles contacts and groups.</p>
* Synchronization manager for CardDAV collections; handles contacts and groups.
*
* <p></p>Group handling differs according to the {@link #groupMethod}. There are two basic methods to
* handle/manage groups:</p>
* <ul>
* <li>{@code CATEGORIES}: groups memberships are attached to each contact and represented as
* Group handling differs according to the {@link #groupMethod}. There are two basic methods to
* handle/manage groups:
*
* 1. CATEGORIES: groups memberships are attached to each contact and represented as
* "category". When a group is dirty or has been deleted, all its members have to be set to
* dirty, too (because they have to be uploaded without the respective category). This
* is done in {@link #prepareDirty()}. Empty groups can be deleted without further processing,
* which is done in {@link #postProcess()} because groups may become empty after downloading
* updated remote contacts.</li>
* <li>Groups as separate VCards: individual and group contacts (with a list of member UIDs) are
* is done in [prepareDirty]. Empty groups can be deleted without further processing,
* which is done in [postProcess] because groups may become empty after downloading
* updated remote contacts.
*
* 2. Groups as separate VCards: individual and group contacts (with a list of member UIDs) are
* distinguished. When a local group is dirty, its members don't need to be set to dirty.
* <ol>
* <li>However, when a contact is dirty, it has
*
* * However, when a contact is dirty, it has
* to be checked whether its group memberships have changed. In this case, the respective
* groups have to be set to dirty. For instance, if contact A is in group G and H, and then
* group membership of G is removed, the contact will be set to dirty because of the changed
* {@link android.provider.ContactsContract.CommonDataKinds.GroupMembership}. DAVdroid will
* [android.provider.ContactsContract.CommonDataKinds.GroupMembership]. DAVdroid will
* then have to check whether the group memberships have actually changed, and if so,
* all affected groups have to be set to dirty. To detect changes in group memberships,
* DAVdroid always mirrors all {@link android.provider.ContactsContract.CommonDataKinds.GroupMembership}
* data rows in respective {@link at.bitfire.vcard4android.CachedGroupMembership} rows.
* DAVdroid always mirrors all [android.provider.ContactsContract.CommonDataKinds.GroupMembership]
* data rows in respective [at.bitfire.vcard4android.CachedGroupMembership] rows.
* If the cached group memberships are not the same as the current group member ships, the
* difference set (in our example G, because its in the cached memberships, but not in the
* actual ones) is marked as dirty. This is done in {@link #prepareDirty()}.</li>
* <li>When downloading remote contacts, groups (+ member information) may be received
* actual ones) is marked as dirty. This is done in [prepareDirty].
*
* * When downloading remote contacts, groups (+ member information) may be received
* by the actual members. Thus, the member lists have to be cached until all VCards
* are received. This is done by caching the member UIDs of each group in
* {@link LocalGroup#COLUMN_PENDING_MEMBERS}. In {@link #postProcess()},
* these "pending memberships" are assigned to the actual contacs and then cleaned up.</li>
* </ol>
* </ul>
* [LocalGroup.COLUMN_PENDING_MEMBERS]. In [postProcess],
* these "pending memberships" are assigned to the actual contacts and then cleaned up.
*/
class ContactsSyncManager(
context: Context,
@@ -118,12 +117,6 @@ class ContactsSyncManager(
}
}
// set up Contacts Provider Settings
val values = ContentValues(2)
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
localAddressBook.updateSettings(values)
collectionURL = HttpUrl.parse(localAddressBook.getURL()) ?: return false
davCollection = DavAddressBook(httpClient.okHttpClient, collectionURL)
@@ -133,7 +126,9 @@ class ContactsSyncManager(
override fun queryCapabilities() {
// prepare remote address book
davCollection.propfind(0, SupportedAddressData.NAME, GetCTag.NAME)
(davCollection.properties[SupportedAddressData.NAME] as SupportedAddressData?)?.let {
val properties = davCollection.properties
properties[SupportedAddressData::class.java]?.let {
hasVCard4 = it.hasVCard4()
}
Logger.log.info("Server advertises VCard/4 support: $hasVCard4")
@@ -239,8 +234,7 @@ class ContactsSyncManager(
.setLocalOnly(true)
.setAutoCancel(true)
.build()
val nm = NotificationUtils.createChannels(context)
nm.notify("discarded_${account.name}", 0, notification)
notificationManager.notify("discarded_${account.name}", 0, notification)
}
override fun prepareUpload(resource: LocalResource): RequestBody {
@@ -293,8 +287,12 @@ class ContactsSyncManager(
remoteResources = HashMap(davCollection.members.size)
for (vCard in davCollection.members) {
// ignore member collections
val type = vCard.properties[ResourceType.NAME] as ResourceType?
if (type != null && type.types.contains(ResourceType.COLLECTION))
var ignore = false
vCard.properties[ResourceType::class.java]?.let { type ->
if (type.types.contains(ResourceType.COLLECTION))
ignore = true
}
if (ignore)
continue
val fileName = vCard.fileName()
@@ -313,9 +311,7 @@ class ContactsSyncManager(
// download new/updated VCards from server
for (bunch in ListUtils.partition(toDownload.toList(), MAX_MULTIGET)) {
if (Thread.interrupted())
return
abortIfCancelled()
Logger.log.info("Downloading ${bunch.joinToString(", ")}")
if (bunch.size == 1) {
@@ -326,7 +322,7 @@ class ContactsSyncManager(
val body = remote.get("text/vcard;version=4.0, text/vcard;charset=utf-8;q=0.8, text/vcard;q=0.5")
// CardDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc6352#section-6.3.2.3]
val eTag = remote.properties[GetETag.NAME] as GetETag?
val eTag = remote.properties[GetETag::class.java]
if (eTag == null || eTag.eTag.isNullOrEmpty())
throw DavException("Received CardDAV GET response without ETag for ${remote.location}")
@@ -344,10 +340,10 @@ class ContactsSyncManager(
for (remote in davCollection.members) {
currentDavResource = remote
val eTag = (remote.properties[GetETag.NAME] as GetETag?)?.eTag
val eTag = remote.properties[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val addressData = remote.properties[AddressData.NAME] as AddressData?
val addressData = remote.properties[AddressData::class.java]
val vCard = addressData?.vCard
?: throw DavException("Received multi-get response without address data")
@@ -484,12 +480,7 @@ class ContactsSyncManager(
}
// authenticate only against a certain host, and only upon request
val username = accountSettings.username()
val password = accountSettings.password()
val builder = if (username != null && password != null)
HttpClient.Builder(context, baseUrl.host(), username, password)
else
HttpClient.Builder(context)
val builder = HttpClient.Builder(context, baseUrl.host(), accountSettings.credentials())
// allow redirects
builder.followRedirects(true)

View File

@@ -5,7 +5,6 @@
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.syncadapter
import android.accounts.AbstractAccountAuthenticator

View File

@@ -32,7 +32,7 @@ import java.util.logging.Level
abstract class SyncAdapterService: Service() {
companion object {
val runningSyncs = Collections.synchronizedSet(mutableSetOf<Pair<String, Account>>())
val runningSyncs = Collections.synchronizedSet(mutableSetOf<Pair<String, Account>>())!!
}
abstract protected fun syncAdapter(): AbstractThreadedSyncAdapter

View File

@@ -20,10 +20,7 @@ import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.exception.*
import at.bitfire.dav4android.property.GetCTag
import at.bitfire.dav4android.property.GetETag
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.App
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.R
import at.bitfire.davdroid.*
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalCollection
import at.bitfire.davdroid.resource.LocalResource
@@ -37,8 +34,11 @@ import okhttp3.HttpUrl
import okhttp3.RequestBody
import java.io.Closeable
import java.io.IOException
import java.io.InterruptedIOException
import java.security.cert.CertificateException
import java.util.*
import java.util.logging.Level
import javax.net.ssl.SSLHandshakeException
abstract class SyncManager(
val context: Context,
@@ -79,6 +79,9 @@ abstract class SyncManager(
protected lateinit var davCollection: DavResource
/** current sync phase */
private var syncPhase: Int = SYNC_PHASE_PREPARE
/** state information for debug info (local resource) */
protected var currentLocalResource: LocalResource? = null
@@ -107,7 +110,6 @@ abstract class SyncManager(
// dismiss previous error notifications
notificationManager.cancel(uniqueCollectionId, notificationId())
var syncPhase = SYNC_PHASE_PREPARE
try {
Logger.log.info("Preparing synchronization")
if (!prepare()) {
@@ -115,8 +117,7 @@ abstract class SyncManager(
return
}
if (Thread.interrupted())
return
abortIfCancelled()
syncPhase = SYNC_PHASE_QUERY_CAPABILITIES
Logger.log.info("Querying capabilities")
queryCapabilities()
@@ -125,8 +126,7 @@ abstract class SyncManager(
Logger.log.info("Processing locally deleted entries")
processLocallyDeleted()
if (Thread.interrupted())
return
abortIfCancelled()
syncPhase = SYNC_PHASE_PREPARE_DIRTY
Logger.log.info("Locally preparing dirty entries")
prepareDirty()
@@ -142,14 +142,12 @@ abstract class SyncManager(
Logger.log.info("Listing local resources")
listLocal()
if (Thread.interrupted())
return
abortIfCancelled()
syncPhase = SYNC_PHASE_LIST_REMOTE
Logger.log.info("Listing remote resources")
listRemote()
if (Thread.interrupted())
return
abortIfCancelled()
syncPhase = SYNC_PHASE_COMPARE_LOCAL_REMOTE
Logger.log.info("Comparing local/remote entries")
compareLocalRemote()
@@ -168,78 +166,104 @@ abstract class SyncManager(
} else
Logger.log.info("Remote collection didn't change, skipping remote sync")
} catch(e: IOException) {
} catch (e: InterruptedException) {
// re-throw to SyncAdapterService
throw e
} catch (e: InterruptedIOException) {
throw e
} catch (e: SSLHandshakeException) {
Logger.log.log(Level.WARNING, "SSL handshake failed", e)
// when a certificate is rejected by cert4android, the cause will be a CertificateException
if (!BuildConfig.customCerts || e.cause !is CertificateException)
notifyException(e)
} catch (e: IOException) {
Logger.log.log(Level.WARNING, "I/O exception during sync, trying again later", e)
syncResult.stats.numIoExceptions++
} catch(e: ServiceUnavailableException) {
} catch (e: ServiceUnavailableException) {
Logger.log.log(Level.WARNING, "Got 503 Service unavailable, trying again later", e)
syncResult.stats.numIoExceptions++
e.retryAfter?.let { retryAfter ->
// how many seconds to wait? getTime() returns ms, so divide by 1000
syncResult.delayUntil = (retryAfter.time - Date().time) / 1000
}
} catch(e: Throwable) {
val messageString: Int
when (e) {
is UnauthorizedException -> {
Logger.log.log(Level.SEVERE, "Not authorized anymore", e)
messageString = R.string.sync_error_unauthorized
syncResult.stats.numAuthExceptions++
}
is HttpException, is DavException -> {
Logger.log.log(Level.SEVERE, "HTTP/DAV Exception during sync", e)
messageString = R.string.sync_error_http_dav
syncResult.stats.numParseExceptions++
}
is CalendarStorageException, is ContactsStorageException -> {
Logger.log.log(Level.SEVERE, "Couldn't access local storage", e)
messageString = R.string.sync_error_local_storage
syncResult.databaseError = true
}
else -> {
Logger.log.log(Level.SEVERE, "Unknown sync error", e)
messageString = R.string.sync_error
syncResult.stats.numParseExceptions++
}
}
val detailsIntent: Intent
if (e is UnauthorizedException) {
detailsIntent = Intent(context, AccountSettingsActivity::class.java)
detailsIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account)
} else {
detailsIntent = Intent(context, DebugInfoActivity::class.java)
detailsIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e)
detailsIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account)
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority)
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase)
currentLocalResource?.let { detailsIntent.putExtra(DebugInfoActivity.KEY_LOCAL_RESOURCE, it.toString()) }
currentDavResource?.let { detailsIntent.putExtra(DebugInfoActivity.KEY_REMOTE_RESOURCE, it.toString()) }
}
// to make the PendingIntent unique
detailsIntent.data = Uri.parse("uri://${javaClass.name}/$uniqueCollectionId")
val builder = NotificationCompat.Builder(context, NotificationUtils.CHANNEL_SYNC_PROBLEMS)
builder .setSmallIcon(R.drawable.ic_sync_error_notification)
.setLargeIcon(App.getLauncherBitmap(context))
.setContentTitle(getSyncErrorTitle())
.setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
try {
val phases = context.resources.getStringArray(R.array.sync_error_phases)
val message = context.getString(messageString, phases[syncPhase])
builder.setContentText(message)
} catch (ex: IndexOutOfBoundsException) {
// should never happen
}
notificationManager.notify(uniqueCollectionId, notificationId(), builder.build())
} catch (e: Throwable) {
notifyException(e)
}
}
private fun notifyException(e: Throwable) {
val messageString: Int
when (e) {
is UnauthorizedException -> {
Logger.log.log(Level.SEVERE, "Not authorized anymore", e)
messageString = R.string.sync_error_unauthorized
syncResult.stats.numAuthExceptions++
}
is HttpException, is DavException -> {
Logger.log.log(Level.SEVERE, "HTTP/DAV Exception during sync", e)
messageString = R.string.sync_error_http_dav
syncResult.stats.numParseExceptions++
}
is CalendarStorageException, is ContactsStorageException -> {
Logger.log.log(Level.SEVERE, "Couldn't access local storage", e)
messageString = R.string.sync_error_local_storage
syncResult.databaseError = true
}
else -> {
Logger.log.log(Level.SEVERE, "Unknown sync error", e)
messageString = R.string.sync_error
syncResult.stats.numParseExceptions++
}
}
val detailsIntent: Intent
if (e is UnauthorizedException) {
detailsIntent = Intent(context, AccountSettingsActivity::class.java)
detailsIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account)
} else {
detailsIntent = Intent(context, DebugInfoActivity::class.java)
detailsIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e)
detailsIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account)
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority)
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase)
currentLocalResource?.let { detailsIntent.putExtra(DebugInfoActivity.KEY_LOCAL_RESOURCE, it.toString()) }
currentDavResource?.let { detailsIntent.putExtra(DebugInfoActivity.KEY_REMOTE_RESOURCE, it.toString()) }
}
// to make the PendingIntent unique
detailsIntent.data = Uri.parse("uri://${javaClass.name}/$uniqueCollectionId")
val builder = NotificationCompat.Builder(context, NotificationUtils.CHANNEL_SYNC_PROBLEMS)
builder .setSmallIcon(R.drawable.ic_sync_error_notification)
.setLargeIcon(App.getLauncherBitmap(context))
.setContentTitle(getSyncErrorTitle())
.setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
try {
val phases = context.resources.getStringArray(R.array.sync_error_phases)
val message = context.getString(messageString, phases[syncPhase])
builder.setContentText(message)
} catch (ex: IndexOutOfBoundsException) {
// should never happen
}
notificationManager.notify(uniqueCollectionId, notificationId(), builder.build())
}
/**
* Throws an [InterruptedException] if the current thread has been interrupted,
* most probably because synchronization was cancelled by the user.
* @throws InterruptedException (which will be catched by [performSync])
* */
protected fun abortIfCancelled() {
if (Thread.interrupted())
throw InterruptedException("Sync was cancelled")
}
override fun close() {
httpClient.close()
}
@@ -336,7 +360,7 @@ abstract class SyncManager(
Logger.log.log(Level.INFO, "Resource has been modified on the server before upload, ignoring", e)
}
val newETag = remote.properties[GetETag.NAME] as GetETag?
val newETag = remote.properties[GetETag::class.java]
val eTag: String?
if (newETag != null) {
eTag = newETag.eTag
@@ -362,7 +386,7 @@ abstract class SyncManager(
*/
protected open fun checkSyncState(): Boolean {
// check CTag (ignore on manual sync)
(davCollection.properties[GetCTag.NAME] as GetCTag?)?.let { remoteCTag = it.cTag }
davCollection.properties[GetCTag::class.java]?.let { remoteCTag = it.cTag }
val localCTag = if (extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL)) {
Logger.log.info("Manual sync, ignoring CTag")
@@ -423,7 +447,7 @@ abstract class SyncManager(
} else {
// contact is still on server, check whether it has been updated remotely
val localETag = local.eTag
val getETag = remote.properties[GetETag.NAME] as GetETag?
val getETag = remote.properties[GetETag::class.java]
val remoteETag = getETag?.eTag ?: throw DavException("Server didn't provide ETag")
if (remoteETag == localETag) {
Logger.log.fine("$name has not been changed on server (ETag still $remoteETag)")

View File

@@ -8,10 +8,18 @@
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.*
import android.content.pm.PackageManager
import android.database.DatabaseUtils
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.NotificationCompat
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.ServiceDB
@@ -19,9 +27,10 @@ import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.model.ServiceDB.Services
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.TaskProvider
import org.dmfs.provider.tasks.TaskContract
import org.dmfs.tasks.contract.TaskContract
import java.util.logging.Level
/**
@@ -38,7 +47,7 @@ class TasksSyncAdapterService: SyncAdapterService() {
override fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
try {
val taskProvider = TaskProvider.fromProviderClient(provider)
val taskProvider = TaskProvider.fromProviderClient(context, provider)
val accountSettings = AccountSettings(context, settings, account)
/* don't run sync if
- sync conditions (e.g. "sync only in WiFi") are not met AND
@@ -55,8 +64,30 @@ class TasksSyncAdapterService: SyncAdapterService() {
it.performSync()
}
}
} catch (e: TaskProvider.ProviderTooOldException) {
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notify = NotificationCompat.Builder(context, NotificationUtils.CHANNEL_SYNC_PROBLEMS)
.setSmallIcon(R.drawable.ic_sync_error_notification)
.setContentTitle(context.getString(R.string.sync_error_opentasks_too_old))
.setContentText(context.getString(R.string.sync_error_opentasks_required_version, e.provider.minVersionName, e.installedVersionName))
.setCategory(NotificationCompat.CATEGORY_ERROR)
try {
val icon = context.packageManager.getApplicationIcon(e.provider.packageName)
if (icon is BitmapDrawable)
notify.setLargeIcon(icon.bitmap)
} catch(ignored: PackageManager.NameNotFoundException) {}
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${e.provider.packageName}"))
if (intent.resolveActivity(context.packageManager) != null)
notify .setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.setAutoCancel(true)
nm.notify(Constants.NOTIFICATION_TASK_SYNC, notify.build())
syncResult.databaseError = true
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't sync task lists", e)
syncResult.databaseError = true
}
Logger.log.info("Task sync complete")

View File

@@ -108,9 +108,7 @@ class TasksSyncManager(
// download new/updated iCalendars from server
for (bunch in ListUtils.partition(toDownload.toList(), MAX_MULTIGET)) {
if (Thread.interrupted())
return
abortIfCancelled()
Logger.log.info("Downloading ${bunch.joinToString(", ")}")
if (bunch.size == 1) {
@@ -121,7 +119,7 @@ class TasksSyncManager(
val body = remote.get("text/calendar")
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
val eTag = remote.properties[GetETag.NAME] as GetETag?
val eTag = remote.properties[GetETag::class.java]
if (eTag == null || eTag.eTag.isNullOrEmpty())
throw DavException("Received CalDAV GET response without ETag for ${remote.location}")
@@ -139,10 +137,10 @@ class TasksSyncManager(
for (remote in davCollection.members) {
currentDavResource = remote
val eTag = (remote.properties[GetETag.NAME] as GetETag?)?.eTag
val eTag = remote.properties[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val calendarData = remote.properties[CalendarData.NAME] as CalendarData?
val calendarData = remote.properties[CalendarData::class.java]
val iCalendar = calendarData?.iCalendar
?: throw DavException("Received multi-get response without task data")

View File

@@ -186,13 +186,13 @@ class AboutActivity: AppCompatActivity() {
val fileName: String
): AsyncTaskLoader<Spanned>(context) {
var content: Spanned? = null
private var content: Spanned? = null
override fun onStartLoading() {
if (content == null)
forceLoad()
else
if (content != null)
deliverResult(content)
else
forceLoad()
}
override fun loadInBackground(): Spanned? {

View File

@@ -363,6 +363,33 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
else
View.GONE
} ?: View.GONE
// ask for permissions
val requiredPermissions = mutableSetOf<String>()
info?.carddav?.let { carddav ->
if (carddav.collections.any { it.type == CollectionInfo.Type.ADDRESS_BOOK }) {
requiredPermissions += Manifest.permission.READ_CONTACTS
requiredPermissions += Manifest.permission.WRITE_CONTACTS
}
}
info?.caldav?.let { caldav ->
if (caldav.collections.any { it.type == CollectionInfo.Type.CALENDAR }) {
requiredPermissions += Manifest.permission.READ_CALENDAR
requiredPermissions += Manifest.permission.WRITE_CALENDAR
if (LocalTaskList.tasksProviderAvailable(this)) {
requiredPermissions += TaskProvider.PERMISSION_READ_TASKS
requiredPermissions += TaskProvider.PERMISSION_WRITE_TASKS
}
}
if (caldav.collections.any { it.type == CollectionInfo.Type.WEBCAL })
requiredPermissions += Manifest.permission.READ_CALENDAR
}
val askPermissions = requiredPermissions.filter { ActivityCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }
if (askPermissions.isNotEmpty())
ActivityCompat.requestPermissions(this, askPermissions.toTypedArray(), 0)
}
override fun onLoaderReset(loader: Loader<AccountInfo>) {
@@ -372,44 +399,58 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
class AccountLoader(
val activity: AccountActivity,
context: Context,
val account: Account
): AsyncTaskLoader<AccountInfo>(activity), DavService.RefreshingStatusListener, ServiceConnection, SyncStatusObserver {
): AsyncTaskLoader<AccountInfo>(context), DavService.RefreshingStatusListener, SyncStatusObserver {
private var syncStatusListener: Any? = null
private var davServiceConn: ServiceConnection? = null
private var davService: DavService.InfoBinder? = null
private lateinit var syncStatusListener: Any
override fun onStartLoading() {
syncStatusListener = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this)
context.bindService(Intent(context, DavService::class.java), this, Context.BIND_AUTO_CREATE)
// get notified when sync status changes
if (syncStatusListener == null)
syncStatusListener = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this)
// bind to DavService to get notified when it's running
if (davServiceConn == null) {
davServiceConn = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
// get notified when DavService is running
davService = service as DavService.InfoBinder
service.addRefreshingStatusListener(this@AccountLoader, false)
onContentChanged()
}
override fun onServiceDisconnected(name: ComponentName) {
davService = null
}
}
context.bindService(Intent(context, DavService::class.java), davServiceConn, Context.BIND_AUTO_CREATE)
} else
forceLoad()
}
override fun onStopLoading() {
davService?.removeRefreshingStatusListener(this)
context.unbindService(this)
override fun onReset() {
ContentResolver.removeStatusChangeListener(syncStatusListener)
}
override fun onServiceConnected(name: ComponentName, service: IBinder) {
davService = service as DavService.InfoBinder
service.addRefreshingStatusListener(this, false)
forceLoad()
}
override fun onServiceDisconnected(name: ComponentName) {
davService = null
davService?.removeRefreshingStatusListener(this)
davServiceConn?.let {
context.unbindService(it)
davServiceConn = null
}
}
override fun onDavRefreshStatusChanged(id: Long, refreshing: Boolean) =
forceLoad()
onContentChanged()
override fun onStatusChanged(which: Int) =
forceLoad()
onContentChanged()
override fun loadInBackground(): AccountInfo {
val info = AccountInfo()
val requiredPermissions = mutableSetOf<String>()
OpenHelper(context).use { dbHelper ->
val db = dbHelper.readableDatabase
@@ -440,7 +481,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
}
carddav.hasHomeSets = hasHomeSets(db, id)
carddav.collections = readCollections(db, id, requiredPermissions)
carddav.collections = readCollections(db, id)
}
Services.SERVICE_CALDAV -> {
val caldav = AccountInfo.ServiceInfo()
@@ -451,17 +492,13 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) ||
ContentResolver.isSyncActive(account, TaskProvider.ProviderName.OpenTasks.authority)
caldav.hasHomeSets = hasHomeSets(db, id)
caldav.collections = readCollections(db, id, requiredPermissions)
caldav.collections = readCollections(db, id)
}
}
}
}
}
val askPermissions = requiredPermissions.filter { ActivityCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED }
if (askPermissions.isNotEmpty())
ActivityCompat.requestPermissions(activity, askPermissions.toTypedArray(), 0)
return info
}
@@ -473,7 +510,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
return false
}
private fun readCollections(db: SQLiteDatabase, service: Long, requiredPermissions: MutableSet<String>): List<CollectionInfo> {
private fun readCollections(db: SQLiteDatabase, service: Long): List<CollectionInfo> {
val collections = LinkedList<CollectionInfo>()
db.query(Collections._TABLE, null, Collections.SERVICE_ID + "=?", arrayOf(service.toString()),
null, null, "${Collections.SUPPORTS_VEVENT} DESC,${Collections.DISPLAY_NAME}").use { cursor ->
@@ -502,19 +539,6 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
}
}
if (collections.any { it.type == CollectionInfo.Type.ADDRESS_BOOK } && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
requiredPermissions += Manifest.permission.READ_CONTACTS
requiredPermissions += Manifest.permission.WRITE_CONTACTS
}
if (collections.any { it.type == CollectionInfo.Type.CALENDAR }) {
requiredPermissions += Manifest.permission.READ_CALENDAR
requiredPermissions += Manifest.permission.WRITE_CALENDAR
requiredPermissions += TaskProvider.PERMISSION_READ_TASKS
requiredPermissions += TaskProvider.PERMISSION_WRITE_TASKS
}
if (collections.any { it.type == CollectionInfo.Type.WEBCAL })
requiredPermissions += Manifest.permission.READ_CALENDAR
return collections
}

View File

@@ -24,7 +24,6 @@ import android.view.ViewGroup
import android.widget.AbsListView
import android.widget.AdapterView
import android.widget.ArrayAdapter
import at.bitfire.davdroid.AccountsChangedReceiver
import at.bitfire.davdroid.R
import kotlinx.android.synthetic.main.account_list_item.view.*
@@ -67,16 +66,28 @@ class AccountListFragment: ListFragment(), LoaderManager.LoaderCallbacks<Array<A
class AccountLoader(
context: Context
): AsyncTaskLoader<Array<Account>>(context), OnAccountsUpdateListener {
): AsyncTaskLoader<Array<Account>>(context) {
override fun onStartLoading() =
AccountsChangedReceiver.registerListener(this, true)
private val accountManager = AccountManager.get(context)!!
private var listener: OnAccountsUpdateListener? = null
override fun onStopLoading() =
AccountsChangedReceiver.unregisterListener(this)
override fun onStartLoading() {
if (listener == null) {
listener = OnAccountsUpdateListener { onContentChanged() }
accountManager.addOnAccountsUpdatedListener(listener, null, false)
}
override fun onAccountsUpdated(accounts: Array<Account>?) =
forceLoad()
forceLoad()
}
override fun onReset() {
listener?.let {
try {
accountManager.removeOnAccountsUpdatedListener(it)
} catch(ignored: IllegalArgumentException) {}
listener = null
}
}
override fun loadInBackground(): Array<Account> =
AccountManager.get(context).getAccountsByType(context.getString(R.string.account_type))

View File

@@ -13,7 +13,10 @@ import android.app.DialogFragment
import android.app.LoaderManager
import android.content.*
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.CalendarContract
import android.security.KeyChain
import android.support.v14.preference.PreferenceFragment
import android.support.v4.app.NavUtils
import android.support.v7.app.AlertDialog
@@ -26,6 +29,7 @@ import android.view.MenuItem
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.GroupMethod
@@ -86,19 +90,46 @@ class AccountSettingsActivity: AppCompatActivity() {
// preference group: authentication
val prefUserName = findPreference("username") as EditTextPreference
prefUserName.summary = accountSettings.username()
prefUserName.text = accountSettings.username()
prefUserName.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
accountSettings.username(newValue as String)
loaderManager.restartLoader(0, arguments, this)
false
}
val prefPassword = findPreference("password") as EditTextPreference
prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
accountSettings.password(newValue as String)
loaderManager.restartLoader(0, arguments, this)
false
val prefCertAlias = findPreference("certificate_alias") as Preference
val credentials = accountSettings.credentials()
when (credentials.type) {
Credentials.Type.UsernamePassword -> {
prefUserName.isVisible = true
prefUserName.summary = credentials.userName
prefUserName.text = credentials.userName
prefUserName.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
accountSettings.credentials(Credentials(newValue as String, credentials.password))
loaderManager.restartLoader(0, arguments, this)
false
}
prefPassword.isVisible = true
prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
accountSettings.credentials(Credentials(credentials.userName, newValue as String))
loaderManager.restartLoader(0, arguments, this)
false
}
prefCertAlias.isVisible = false
}
Credentials.Type.ClientCertificate -> {
prefUserName.isVisible = false
prefPassword.isVisible = false
prefCertAlias.isVisible = true
prefCertAlias.summary = credentials.certificateAlias
prefCertAlias.setOnPreferenceClickListener {
KeyChain.choosePrivateKeyAlias(activity, { alias ->
accountSettings.credentials(Credentials(certificateAlias = alias))
Handler(Looper.getMainLooper()).post({
loaderManager.restartLoader(0, arguments, this)
})
}, null, null, null, -1, credentials.certificateAlias)
true
}
}
}
// preference group: sync
@@ -175,7 +206,7 @@ class AccountSettingsActivity: AppCompatActivity() {
else
prefWifiOnlySSIDs.setSummary(R.string.settings_sync_wifi_only_ssids_off)
prefWifiOnlySSIDs.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
accountSettings.setSyncWifiOnlySSIDs((newValue as String).split(',').map { StringUtils.trimToNull(it) }.filterNotNull().distinct())
accountSettings.setSyncWifiOnlySSIDs((newValue as String).split(',').mapNotNull { StringUtils.trimToNull(it) }.distinct())
loaderManager.restartLoader(0, arguments, this)
false
}
@@ -293,17 +324,22 @@ class AccountSettingsActivity: AppCompatActivity() {
val account: Account
): SettingsLoader<Pair<ISettings, AccountSettings>?>(context), SyncStatusObserver {
var listenerHandle: Any? = null
private var listenerHandle: Any? = null
override fun onStartLoading() {
super.onStartLoading()
listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this@AccountSettingsLoader)
if (listenerHandle == null)
listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this@AccountSettingsLoader)
}
override fun onStopLoading() {
super.onStopLoading()
listenerHandle?.let { ContentResolver.removeStatusChangeListener(it) }
listenerHandle = null
override fun onReset() {
super.onReset()
listenerHandle?.let {
ContentResolver.removeStatusChangeListener(it)
listenerHandle = null
}
}
override fun loadInBackground(): Pair<ISettings, AccountSettings>? {
@@ -320,7 +356,7 @@ class AccountSettingsActivity: AppCompatActivity() {
}
override fun onStatusChanged(which: Int) {
forceLoad()
onContentChanged()
}
}

View File

@@ -121,7 +121,7 @@ class CreateCalendarActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks
// select system time zone
val defaultTimeZone = TimeZone.getDefault().id
for (i in 0 .. timeZones.size - 1)
for (i in 0 until timeZones.size)
if (timeZones[i] == defaultTimeZone) {
time_zone.setSelection(i)
break

View File

@@ -23,6 +23,7 @@ import android.provider.ContactsContract
import android.support.v4.content.ContextCompat
import android.support.v4.content.FileProvider
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import at.bitfire.dav4android.exception.HttpException
@@ -37,7 +38,6 @@ import at.bitfire.davdroid.settings.Settings
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.ContactsStorageException
import kotlinx.android.synthetic.main.activity_debug_info.*
import org.apache.commons.lang3.exception.ExceptionUtils
import java.io.File
import java.io.FileWriter
import java.io.IOException
@@ -120,9 +120,17 @@ class DebugInfoActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<Stri
val extras: Bundle?
): AsyncTaskLoader<String>(context) {
override fun onStartLoading() = forceLoad()
var result: String? = null
override fun onStartLoading() {
if (result != null)
deliverResult(result)
else
forceLoad()
}
override fun loadInBackground(): String {
Logger.log.info("Building debug info")
val report = StringBuilder("--- BEGIN DEBUG INFO ---\n")
// begin with most specific information
@@ -155,7 +163,7 @@ class DebugInfoActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<Stri
}
throwable?.let {
report.append("\nEXCEPTION:\n${ExceptionUtils.getStackTrace(throwable)}")
report.append("\nEXCEPTION:\n${Log.getStackTraceString(throwable)}")
}
// logs (for instance, from failed resource detection)
@@ -270,7 +278,11 @@ class DebugInfoActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<Stri
}
report.append("--- END DEBUG INFO ---\n")
return report.toString()
report.toString().let {
result = it
return it
}
}
private fun syncStatus(settings: AccountSettings, authority: String): String {

View File

@@ -35,12 +35,12 @@ class PermissionsActivity: AppCompatActivity() {
private fun refresh() {
val noCalendarPermissions =
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED;
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED
calendar_permissions.visibility = if (noCalendarPermissions) View.VISIBLE else View.GONE
val noContactsPermissions =
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED;
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED
contacts_permissions.visibility = if (noContactsPermissions) View.VISIBLE else View.GONE
val noTaskPermissions: Boolean
@@ -50,7 +50,7 @@ class PermissionsActivity: AppCompatActivity() {
ActivityCompat.checkSelfPermission(this, TaskProvider.PERMISSION_WRITE_TASKS) != PackageManager.PERMISSION_GRANTED
findViewById<View>(R.id.opentasks_permissions).visibility = if (noTaskPermissions) View.VISIBLE else View.GONE
} else {
findViewById<View>(R.id.opentasks_permissions).visibility = View.GONE;
findViewById<View>(R.id.opentasks_permissions).visibility = View.GONE
noTaskPermissions = false
}

View File

@@ -23,33 +23,39 @@ abstract class SettingsLoader<T>(
val settingsObserver = object: ISettingsObserver.Stub() {
override fun onSettingsChanged() {
handler.post {
forceLoad()
onContentChanged()
}
}
}
private var settingsSvc: ServiceConnection? = null
var settings: ISettings? = null
private val settingsSvc = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
settings = ISettings.Stub.asInterface(binder)
settings!!.registerObserver(settingsObserver)
forceLoad()
}
override fun onServiceDisconnected(name: ComponentName?) {
settings!!.unregisterObserver(settingsObserver)
settings = null
}
}
override fun onStartLoading() {
context.bindService(Intent(context, Settings::class.java), settingsSvc, Context.BIND_AUTO_CREATE)
if (settingsSvc != null)
forceLoad()
else {
settingsSvc = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
settings = ISettings.Stub.asInterface(binder)
settings!!.registerObserver(settingsObserver)
onContentChanged()
}
override fun onServiceDisconnected(name: ComponentName?) {
settings!!.unregisterObserver(settingsObserver)
settings = null
}
}
context.bindService(Intent(context, Settings::class.java), settingsSvc, Context.BIND_AUTO_CREATE)
}
}
override fun onStopLoading() {
context.unbindService(settingsSvc)
override fun onReset() {
settingsSvc?.let {
context.unbindService(it)
settingsSvc = null
}
}
}

View File

@@ -146,7 +146,8 @@ class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISe
.setMessage(R.string.startup_google_play_accounts_removed_message)
.setPositiveButton(android.R.string.ok, { _, _ -> })
.setNeutralButton(R.string.startup_google_play_accounts_removed_more_info, { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.navigation_drawer_faq_url)))
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.homepage_url)).buildUpon()
.appendPath("faq").appendPath("accounts-gone-after-reboot-or-update").build())
activity.startActivity(intent)
})
.setNegativeButton(R.string.startup_dont_show_again, { _, _ ->

View File

@@ -62,10 +62,9 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
val config = arguments.getSerializable(KEY_CONFIG) as DavResourceFinder.Configuration
v.account_name.setText(if (config.calDAV != null && config.calDAV.email != null)
config.calDAV.email
else
config.userName)
v.account_name.setText(config.calDAV?.email ?:
config.credentials.userName ?:
config.credentials.certificateAlias)
// CardDAV-specific
v.carddav.visibility = if (config.cardDAV != null) View.VISIBLE else View.GONE
@@ -125,11 +124,11 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
val settings = settings ?: return false
// create Android account
val userData = AccountSettings.initialUserData(config.userName)
val userData = AccountSettings.initialUserData(config.credentials)
Logger.log.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, userData))
val accountManager = AccountManager.get(activity)
if (!accountManager.addAccountExplicitly(account, config.password, userData))
if (!accountManager.addAccountExplicitly(account, config.credentials.password, userData))
return false
// add entries for account to service DB

View File

@@ -17,6 +17,7 @@ import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.log.StringHandler
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.settings.Settings
import okhttp3.HttpUrl
import org.apache.commons.lang3.builder.ReflectionToStringBuilder
@@ -34,7 +35,7 @@ import java.util.logging.Logger
class DavResourceFinder(
val context: Context,
private val credentials: LoginCredentials
private val loginInfo: LoginInfo
): Closeable {
enum class Service(val wellKnownName: String) {
@@ -53,13 +54,13 @@ class DavResourceFinder(
private val settings = Settings.getInstance(context)
private val httpClient: HttpClient = HttpClient.Builder(context, settings, logger = log)
.addAuthentication(null, credentials.userName, credentials.password)
.addAuthentication(null, loginInfo.credentials)
.setForeground(true)
.build()
fun cancel() {
log.warning("Shutting down resource detection")
httpClient.okHttpClient.dispatcher().executorService().shutdownNow()
httpClient.okHttpClient.dispatcher().executorService().shutdown()
httpClient.okHttpClient.connectionPool().evictAll()
}
@@ -74,7 +75,7 @@ class DavResourceFinder(
val calDavConfig = findInitialConfiguration(Service.CALDAV)
return Configuration(
credentials.userName, credentials.password,
loginInfo.credentials,
cardDavConfig, calDavConfig,
logBuffer.toString()
)
@@ -82,7 +83,7 @@ class DavResourceFinder(
private fun findInitialConfiguration(service: Service): Configuration.ServiceInfo? {
// user-given base URI (either mailto: URI or http(s):// URL)
val baseURI = credentials.uri
val baseURI = loginInfo.uri
// domain for service discovery
var discoveryFQDN: String? = null
@@ -131,7 +132,7 @@ class DavResourceFinder(
val davPrincipal = DavResource(httpClient.okHttpClient, HttpUrl.get(config.principal)!!, log)
try {
davPrincipal.propfind(0, CalendarUserAddressSet.NAME)
(davPrincipal.properties[CalendarUserAddressSet.NAME] as CalendarUserAddressSet?)?.let { addressSet ->
davPrincipal.properties[CalendarUserAddressSet::class.java]?.let { addressSet ->
for (href in addressSet.hrefs)
try {
val uri = URI(href)
@@ -181,18 +182,16 @@ class DavResourceFinder(
}
// check for current-user-principal
davBase.findProperty(CurrentUserPrincipal.NAME)?.let { (dav, second) ->
val currentUserPrincipal = second as CurrentUserPrincipal?
currentUserPrincipal?.href?.let {
davBase.findProperty(CurrentUserPrincipal::class.java)?.let { (dav, currentUserPrincipal) ->
currentUserPrincipal.href?.let {
principal = dav.location.resolve(it)
}
}
// check for resource type "principal"
if (principal == null)
for ((dav, second) in davBase.findProperties(ResourceType.NAME)) {
val resourceType = second as ResourceType?
if (resourceType != null && resourceType.types.contains(ResourceType.PRINCIPAL)) {
for ((dav, resourceType) in davBase.findProperties(ResourceType::class.java)) {
if (resourceType.types.contains(ResourceType.PRINCIPAL)) {
principal = dav.location
break
}
@@ -217,8 +216,7 @@ class DavResourceFinder(
*/
fun rememberIfAddressBookOrHomeset(dav: DavResource, config: Configuration.ServiceInfo) {
// Is there an address book?
for ((addressBook, second) in dav.findProperties(ResourceType.NAME)) {
val resourceType = second as ResourceType
for ((addressBook, resourceType) in dav.findProperties(ResourceType::class.java)) {
if (resourceType.types.contains(ResourceType.ADDRESSBOOK)) {
addressBook.location = UrlUtils.withTrailingSlash(addressBook.location)
log.info("Found address book at ${addressBook.location}")
@@ -227,8 +225,7 @@ class DavResourceFinder(
}
// Is there an addressbook-home-set?
for ((dav, second) in dav.findProperties(AddressbookHomeSet.NAME)) {
val homeSet = second as AddressbookHomeSet
for ((dav, homeSet) in dav.findProperties(AddressbookHomeSet::class.java)) {
for (href in homeSet.hrefs) {
dav.location.resolve(href)?.let {
val location = UrlUtils.withTrailingSlash(it)
@@ -239,10 +236,9 @@ class DavResourceFinder(
}
}
fun rememberIfCalendarOrHomeset(dav: DavResource, config: Configuration.ServiceInfo) {
private fun rememberIfCalendarOrHomeset(dav: DavResource, config: Configuration.ServiceInfo) {
// Is the collection a calendar collection?
for ((calendar, second) in dav.findProperties(ResourceType.NAME)) {
val resourceType = second as ResourceType
for ((calendar, resourceType) in dav.findProperties(ResourceType::class.java)) {
if (resourceType.types.contains(ResourceType.CALENDAR)) {
calendar.location = UrlUtils.withTrailingSlash(calendar.location)
log.info("Found calendar at ${calendar.location}")
@@ -251,8 +247,7 @@ class DavResourceFinder(
}
// Is there an calendar-home-set?
for ((dav, second) in dav.findProperties(CalendarHomeSet.NAME)) {
val homeSet = second as CalendarHomeSet
for ((dav, homeSet) in dav.findProperties(CalendarHomeSet::class.java)) {
for (href in homeSet.hrefs) {
dav.location.resolve(href)?.let {
val location = UrlUtils.withTrailingSlash(it)
@@ -299,7 +294,9 @@ class DavResourceFinder(
val query = "_${service.wellKnownName}s._tcp.$domain"
log.fine("Looking up SRV records for $query")
val srv = DavUtils.selectSRVRecord(Lookup(query, Type.SRV).run())
val srvLookup = Lookup(query, Type.SRV)
DavUtils.prepareLookup(context, srvLookup)
val srv = DavUtils.selectSRVRecord(srvLookup.run())
if (srv != null) {
// choose SRV record to use (query may return multiple SRV records)
scheme = "https"
@@ -315,7 +312,9 @@ class DavResourceFinder(
}
// look for TXT record too (for initial context path)
paths.addAll(DavUtils.pathsFromTXTRecords(Lookup(query, Type.TXT).run()))
val txtLookup = Lookup(query, Type.TXT)
DavUtils.prepareLookup(context, txtLookup)
paths.addAll(DavUtils.pathsFromTXTRecords(txtLookup.run()))
// if there's TXT record and if it it's wrong, try well-known
paths.add("/.well-known/" + service.wellKnownName)
@@ -351,8 +350,7 @@ class DavResourceFinder(
val dav = DavResource(httpClient.okHttpClient, url, log)
dav.propfind(0, CurrentUserPrincipal.NAME)
dav.findProperty(CurrentUserPrincipal.NAME)?.let { (dav, second) ->
val currentUserPrincipal = second as CurrentUserPrincipal
dav.findProperty(CurrentUserPrincipal::class.java)?.let { (dav, currentUserPrincipal) ->
currentUserPrincipal.href?.let { href ->
dav.location.resolve(href)?.let { principal ->
log.info("Found current-user-principal: $principal")
@@ -374,8 +372,7 @@ class DavResourceFinder(
// data classes
class Configuration(
val userName: String,
val password: String,
val credentials: Credentials,
val cardDAV: ServiceInfo?,
val calDAV: ServiceInfo?,

View File

@@ -23,7 +23,7 @@ class DetectConfigurationFragment: DialogFragment(), LoaderManager.LoaderCallbac
companion object {
val ARG_LOGIN_CREDENTIALS = "credentials"
fun newInstance(credentials: LoginCredentials): DetectConfigurationFragment {
fun newInstance(credentials: LoginInfo): DetectConfigurationFragment {
val frag = DetectConfigurationFragment()
val args = Bundle(1)
args.putParcelable(ARG_LOGIN_CREDENTIALS, credentials)
@@ -111,18 +111,18 @@ class DetectConfigurationFragment: DialogFragment(), LoaderManager.LoaderCallbac
class ServerConfigurationLoader(
context: Context,
val credentials: LoginCredentials
private val credentials: LoginInfo
): AsyncTaskLoader<Configuration>(context) {
var resourceFinder: DavResourceFinder? = null
override fun onStartLoading() = forceLoad()
override fun onCancelLoad(): Boolean {
override fun cancelLoadInBackground() {
thread {
resourceFinder?.cancel()
resourceFinder = null
}
return true
}
override fun loadInBackground(): Configuration {

View File

@@ -10,34 +10,35 @@ package at.bitfire.davdroid.ui.setup
import android.os.Parcel
import android.os.Parcelable
import at.bitfire.davdroid.model.Credentials
import java.net.URI
data class LoginCredentials(
val uri: URI,
val userName: String,
val password: String
data class LoginInfo(
@JvmField val uri: URI,
@JvmField val credentials: Credentials
): Parcelable {
constructor(uri: URI, userName: String? = null, password: String? = null, certificateAlias: String? = null):
this(uri, Credentials(userName, password, certificateAlias))
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeSerializable(uri)
dest.writeString(userName)
dest.writeString(password)
dest.writeSerializable(credentials)
}
companion object {
@JvmField
val CREATOR = object: Parcelable.Creator<LoginCredentials> {
val CREATOR = object: Parcelable.Creator<LoginInfo> {
override fun createFromParcel(source: Parcel) =
LoginCredentials(
LoginInfo(
source.readSerializable() as URI,
source.readString(),
source.readString()
source.readSerializable() as Credentials
)
override fun newArray(size: Int) = arrayOfNulls<LoginCredentials>(size)
override fun newArray(size: Int) = arrayOfNulls<LoginInfo>(size)
}
}

View File

@@ -1,49 +0,0 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.ui.widget
import android.content.Context
import android.util.AttributeSet
import android.view.inputmethod.EditorInfo
import android.widget.CompoundButton
import android.widget.LinearLayout
import at.bitfire.davdroid.R
import kotlinx.android.synthetic.main.edit_password.view.*
class EditPassword(
context: Context,
attrs: AttributeSet?
): LinearLayout(context, attrs) {
val NS_ANDROID = "http://schemas.android.com/apk/res/android";
constructor(context: Context): this(context, null)
init {
inflate(context, R.layout.edit_password, this)
attrs?.let {
password.setHint(it.getAttributeResourceValue(NS_ANDROID, "hint", 0))
password.setText(it.getAttributeValue(NS_ANDROID, "text"))
}
show_password.setOnCheckedChangeListener({ _: CompoundButton, isChecked: Boolean ->
var inputType = password.inputType and EditorInfo.TYPE_MASK_VARIATION.inv()
inputType = inputType or if (isChecked) EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD else EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
password.inputType = inputType
})
}
fun getText() = password.text!!
fun setText(text: CharSequence?) = password.setText(text)
fun setError(error: CharSequence?) {
password.error = error
}
}

View File

@@ -37,7 +37,7 @@ class MaximizedListView(
else {
adapter?.let { listAdapter ->
val widthSpec = View.MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
for (i in 0 .. listAdapter.count - 1) {
for (i in 0 until listAdapter.count) {
val listItem = listAdapter.getView(i, null, this)
listItem.measure(widthSpec, View.MeasureSpec.UNSPECIFIED)
height += listItem.measuredHeight

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2013 2016 Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true"
android:drawable="@drawable/ic_visibility_dark" />
<item android:state_checked="false"
android:drawable="@drawable/ic_visibility_off_dark" />
</selector>

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2013 2016 Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:typeface="monospace"
android:inputType="textPassword"/>
<CheckBox
android:id="@+id/show_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:button="@drawable/password_eye_button"
android:checked="false"/>
</LinearLayout>

View File

@@ -42,6 +42,10 @@
android:id="@+id/nav_website"
android:icon="@drawable/ic_home_dark"
android:title="@string/navigation_drawer_website"/>
<item
android:id="@+id/nav_manual"
android:icon="@drawable/ic_info_dark"
android:title="@string/navigation_drawer_manual"/>
<item
android:id="@+id/nav_faq"
android:icon="@drawable/ic_help_dark"

View File

@@ -9,6 +9,9 @@
<string name="please_wait">Bitte warten …</string>
<string name="send">Senden</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">Fehlersuche</string>
<string name="notification_channel_sync_status">Sync-Status</string>
<string name="notification_channel_sync_problems">Sync-Probleme</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Akku-Leistungsoptimierung</string>
<string name="startup_battery_optimization_message">Android kann die DAVdroid-Synchronisierung in ein paar Tagen reduzieren/deaktivieren. Um dies zu verhindern, muss die Akku-Leistungsoptimierung deaktiviert werden.</string>
@@ -38,11 +41,12 @@
<string name="navigation_drawer_close">Hauptmenü schließen</string>
<string name="navigation_drawer_subtitle">CalDAV/CardDAV-Sync-Adapter</string>
<string name="navigation_drawer_about">Über / Lizenz</string>
<string name="navigation_drawer_beta_feedback">Beta-Diskussion</string>
<string name="navigation_drawer_beta_feedback">Beta-Rückmeldung</string>
<string name="navigation_drawer_settings">Einstellungen</string>
<string name="navigation_drawer_news_updates">Aktuelles</string>
<string name="navigation_drawer_external_links">Externe Links</string>
<string name="navigation_drawer_website">Homepage</string>
<string name="navigation_drawer_manual">Handbuch</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_forums">Hilfe / Forum</string>
<string name="navigation_drawer_donate">Spenden</string>
@@ -121,10 +125,13 @@
<string name="login_password_required">Passwort wird benötigt</string>
<string name="login_type_url">Mit URL und Benutzername anmelden</string>
<string name="login_url_must_be_http_or_https">URL muss mit http(s):// beginnen</string>
<string name="login_url_must_be_https">URL muss mit https:// beginnen</string>
<string name="login_url_host_name_required">Hostname wird benötigt</string>
<string name="login_user_name">Benutzername</string>
<string name="login_user_name_required">Benutzername wird benötigt</string>
<string name="login_base_url">Basis-URL</string>
<string name="login_type_url_certificate">Mit URL und Client-Zertifikat anmelden</string>
<string name="login_select_certificate">Zertifikat auswählen</string>
<string name="login_login">Anmelden</string>
<string name="login_back">Zurück</string>
<string name="login_create_account">Konto anlegen</string>
@@ -145,6 +152,7 @@
<string name="settings_password">Passwort</string>
<string name="settings_password_summary">Aktualisieren Sie Ihr Passwort gemäß den Server-Einstellungen.</string>
<string name="settings_enter_password">Passwort eingeben:</string>
<string name="settings_certificate_alias">Client-Zertifikat (Alias)</string>
<string name="settings_sync">Synchronisierung</string>
<string name="settings_sync_interval_contacts">Häufigkeit der Kontakte-Synchronisierung</string>
<string name="settings_sync_summary_manually">Nur manuell</string>
@@ -230,6 +238,8 @@
</plurals>
<string name="sync_error_permissions">DAVdroid-Berechtigungen</string>
<string name="sync_error_permissions_text">Zusätzliche Berechtigungen benötigt</string>
<string name="sync_error_opentasks_too_old">OpenTasks zu alt</string>
<string name="sync_error_opentasks_required_version">Version %1$s benötigt (derzeit %2$s)</string>
<string name="sync_error_calendar">Kalender-Synchronisierung fehlgeschlagen (%s)</string>
<string name="sync_error_contacts">Adressbuch-Synchronisierung fehlgeschlagen (%s)</string>
<string name="sync_error_tasks">Aufgaben-Synchronisierung fehlgeschlagen (%s)</string>

View File

@@ -64,8 +64,8 @@
<string name="navigation_drawer_news_updates">News &amp; updates</string>
<string name="navigation_drawer_external_links">External links</string>
<string name="navigation_drawer_website">Web site</string>
<string name="navigation_drawer_manual">Manual</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_faq_url">https://www.davdroid.com/faq/?pk_campaign=davdroid-app</string>
<string name="navigation_drawer_forums">Help / Forums</string>
<string name="navigation_drawer_donate">Donate</string>
<string name="account_list_empty">Welcome to DAVdroid!\n\nYou can add a CalDAV/CardDAV account now.</string>
@@ -149,10 +149,13 @@
<string name="login_password_required">Password required</string>
<string name="login_type_url">Login with URL and user name</string>
<string name="login_url_must_be_http_or_https">URL must begin with http(s)://</string>
<string name="login_url_must_be_https">URL must begin with https://</string>
<string name="login_url_host_name_required">Host name required</string>
<string name="login_user_name">User name</string>
<string name="login_user_name_required">User name required</string>
<string name="login_base_url">Base URL</string>
<string name="login_type_url_certificate">Login with URL and client certificate</string>
<string name="login_select_certificate">Select certificate</string>
<string name="login_login">Login</string>
<string name="login_back">Back</string>
<string name="login_create_account">Create account</string>
@@ -175,6 +178,7 @@
<string name="settings_password">Password</string>
<string name="settings_password_summary">Update the password according to your server.</string>
<string name="settings_enter_password">Enter your password:</string>
<string name="settings_certificate_alias">Client certificate alias</string>
<string name="settings_sync">Synchronization</string>
<string name="settings_sync_interval_contacts">Contacts sync. interval</string>
<string name="settings_sync_summary_manually">Only manually</string>
@@ -273,6 +277,8 @@
</plurals>
<string name="sync_error_permissions">DAVdroid permissions</string>
<string name="sync_error_permissions_text">Additional permissions required</string>
<string name="sync_error_opentasks_too_old">OpenTasks too old</string>
<string name="sync_error_opentasks_required_version">Required version: %1$s (currently %2$s)</string>
<string name="sync_error_calendar">Calendar synchronization failed (%s)</string>
<string name="sync_error_contacts">Address book synchronization failed (%s)</string>
<string name="sync_error_tasks">Task synchronization failed (%s)</string>

View File

@@ -25,6 +25,11 @@
android:summary="@string/settings_password_summary"
android:dialogTitle="@string/settings_enter_password"/>
<Preference
android:key="certificate_alias"
android:title="@string/settings_certificate_alias"
android:persistent="false"/>
</PreferenceCategory>
<PreferenceCategory

View File

@@ -9,7 +9,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.dokka_version = '0.9.15'
ext.kotlin_version = '1.2.0'
ext.kotlin_version = '1.2.20'
repositories {
jcenter()