mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2026-01-06 13:57:54 -05:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c43f016dc7 | ||
|
|
c45e4a1797 | ||
|
|
3a734c9e68 | ||
|
|
e0e4a026e6 | ||
|
|
6f6182c0ce | ||
|
|
4158ec9aee | ||
|
|
c49333998f | ||
|
|
bc4f4b5dfd | ||
|
|
dbd5bde458 | ||
|
|
be4c680497 | ||
|
|
d7c5ed23b7 | ||
|
|
25a328a3c4 | ||
|
|
dd3d95bdb9 | ||
|
|
0a47935430 | ||
|
|
2a83f98f0a | ||
|
|
2224a6d2ae | ||
|
|
30968f8ee3 | ||
|
|
7f9be9a8da | ||
|
|
a516800f45 | ||
|
|
0c92b02d73 | ||
|
|
95354096ab | ||
|
|
d8a54f823b | ||
|
|
ceedd218ca | ||
|
|
1627770103 | ||
|
|
82b1da5f0d | ||
|
|
5e70b97942 | ||
|
|
43e216642b | ||
|
|
4d17bc673f | ||
|
|
cf24bfa965 | ||
|
|
3d746e7019 | ||
|
|
9a30207316 | ||
|
|
dcb38e89a3 | ||
|
|
2aed1ee97d | ||
|
|
0a87a15822 | ||
|
|
0bca883d76 | ||
|
|
e4c282cd99 |
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
24
app/src/androidTest/resources/sample.crt
Normal file
24
app/src/androidTest/resources/sample.crt
Normal 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-----
|
||||
BIN
app/src/androidTest/resources/sample.key
Normal file
BIN
app/src/androidTest/resources/sample.key
Normal file
Binary file not shown.
@@ -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()))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">ニュース & 更新</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">Новости & обновления</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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
41
app/src/main/java/at/bitfire/davdroid/model/Credentials.kt
Normal file
41
app/src/main/java/at/bitfire/davdroid/model/Credentials.kt
Normal 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)"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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, { _, _ ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
<string name="navigation_drawer_news_updates">News & 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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Submodule cert4android updated: b10d431c5c...551580e987
Submodule dav4android updated: b4b0e6bb86...a3fd1ac91e
Submodule ical4android updated: bff8e5b885...d0adb639da
Submodule vcard4android updated: ff4ef65418...e3ff8b1999
Reference in New Issue
Block a user