diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 95d6fb9c5..6de885757 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -112,6 +112,14 @@ android { setDimension("store") } } + + dataBinding { + isEnabled = true + } + + packagingOptions { + exclude("META-INF/*.kotlin_module") + } } configure { @@ -136,6 +144,7 @@ dependencies { implementation("com.gitlab.bitfireAT:ical4android:be6d515db8") { exclude(group = "org.threeten", module = "threetenbp") } + implementation("com.gitlab.bitfireAT:cert4android:1488e39a66") annotationProcessor("com.google.dagger:dagger-compiler:${Versions.dagger}") implementation("com.google.dagger:dagger:${Versions.dagger}") diff --git a/app/licenses.yml b/app/licenses.yml index 84c12f0ef..21c3ebf49 100644 --- a/app/licenses.yml +++ b/app/licenses.yml @@ -8,6 +8,11 @@ copyrightHolder: bitfire web engineering (Ricki Hirner, Bernhard Stockmann) license: GNU General Public License, Version 3.0 licenseUrl: https://www.gnu.org/licenses/gpl.txt +- artifact: com.gitlab.bitfireAT:cert4android:+ + name: cert4android + copyrightHolder: bitfire web engineering (Ricki Hirner, Bernhard Stockmann) + licenseUrl: https://www.gnu.org/licenses/gpl.txt + license: GNU General Public License, Version 3.0 - artifact: androidx.coordinatorlayout:coordinatorlayout:+ name: Android Support Library Coordinator Layout copyrightHolder: Android Open Source Project @@ -738,3 +743,61 @@ license: The Apache Software License, Version 2.0 licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt url: https://developer.android.com/topic/libraries/architecture/index.html +- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-core-common:+ + name: kotlinx-coroutines-core-common + copyrightHolder: JetBrains s.r.o. + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://github.com/Kotlin/kotlinx.coroutines +- artifact: org.conscrypt:conscrypt-android:+ + name: org.conscrypt:conscrypt-android + copyrightHolder: Android Open Source Project + license: Apache 2 + licenseUrl: https://www.apache.org/licenses/LICENSE-2.0 + url: https://conscrypt.org/ +- artifact: org.jetbrains.kotlin:kotlin-stdlib-jdk8:+ + name: org.jetbrains.kotlin:kotlin-stdlib-jdk8 + copyrightHolder: JetBrains s.r.o. + license: The Apache License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://kotlinlang.org/ +- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-android:+ + name: kotlinx-coroutines-android + copyrightHolder: JetBrains s.r.o. + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://github.com/Kotlin/kotlinx.coroutines +- artifact: androidx.databinding:databinding-adapters:+ + name: databinding-adapters + copyrightHolder: Android Open Source Project + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt +- artifact: androidx.lifecycle:lifecycle-viewmodel-ktx:+ + name: Android Lifecycle ViewModel Kotlin Extensions + copyrightHolder: Android Open Source Project + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.annotation:annotation-experimental:+ + name: Experimental annotation + copyrightHolder: Android Open Source Project + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/jetpack/androidx +- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-core:+ + name: kotlinx-coroutines-core + copyrightHolder: JetBrains s.r.o. + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://github.com/Kotlin/kotlinx.coroutines +- artifact: androidx.databinding:databinding-common:+ + name: Data Binding Base Library + copyrightHolder: Android Open Source Project + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/studio +- artifact: androidx.databinding:databinding-runtime:+ + name: databinding-runtime + copyrightHolder: Android Open Source Project + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt diff --git a/app/src/androidTest/java/org/tasks/caldav/CaldavClientTest.java b/app/src/androidTest/java/org/tasks/caldav/CaldavClientTest.java index 244d18b19..b0127b24b 100644 --- a/app/src/androidTest/java/org/tasks/caldav/CaldavClientTest.java +++ b/app/src/androidTest/java/org/tasks/caldav/CaldavClientTest.java @@ -1,8 +1,9 @@ package org.tasks.caldav; -import static androidx.test.InstrumentationRegistry.getTargetContext; import static org.tasks.injection.TestModule.newPreferences; +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; @@ -12,7 +13,8 @@ public class CaldavClientTest { @Test public void dontCrashOnSpaceInUrl() { - new CaldavClient(null, newPreferences(getTargetContext()), null) + Context context = ApplicationProvider.getApplicationContext(); + new CaldavClient(context, null, newPreferences(context), null) .forUrl("https://example.com/remote.php/a space/", "username", "password"); } } diff --git a/app/src/debug/java/org/tasks/preferences/DebugPreferences.java b/app/src/debug/java/org/tasks/preferences/DebugPreferences.java index da1ea4006..5707540cb 100644 --- a/app/src/debug/java/org/tasks/preferences/DebugPreferences.java +++ b/app/src/debug/java/org/tasks/preferences/DebugPreferences.java @@ -5,6 +5,7 @@ import static com.google.common.primitives.Ints.asList; import android.os.Bundle; import android.preference.Preference; import androidx.annotation.StringRes; +import at.bitfire.cert4android.CustomCertManager; import com.android.billingclient.api.BillingClient.SkuType; import javax.inject.Inject; import org.tasks.R; @@ -12,11 +13,13 @@ import org.tasks.billing.BillingClient; import org.tasks.billing.Inventory; import org.tasks.injection.ActivityComponent; import org.tasks.injection.InjectingPreferenceActivity; +import org.tasks.ui.Toaster; public class DebugPreferences extends InjectingPreferenceActivity { @Inject Inventory inventory; @Inject BillingClient billingClient; + @Inject Toaster toaster; @Override public void onCreate(Bundle savedInstanceState) { @@ -37,6 +40,14 @@ public class DebugPreferences extends InjectingPreferenceActivity { }); } + findPreference(R.string.debug_reset_ssl) + .setOnPreferenceClickListener( + preference -> { + CustomCertManager.Companion.resetCertificates(this); + toaster.longToast("SSL certificates reset"); + return false; + }); + setupIap(R.string.debug_themes, Inventory.SKU_THEMES); setupIap(R.string.debug_tasker, Inventory.SKU_TASKER); setupIap(R.string.debug_dashclock, Inventory.SKU_DASHCLOCK); diff --git a/app/src/debug/res/values/keys.xml b/app/src/debug/res/values/keys.xml index d472f4a34..e26599f05 100644 --- a/app/src/debug/res/values/keys.xml +++ b/app/src/debug/res/values/keys.xml @@ -10,4 +10,5 @@ debug_themes debug_tasker debug_dashclock + Reset SSL certificates \ No newline at end of file diff --git a/app/src/debug/res/xml/preferences_debug.xml b/app/src/debug/res/xml/preferences_debug.xml index e1c5a1c8a..f80aa00a4 100644 --- a/app/src/debug/res/xml/preferences_debug.xml +++ b/app/src/debug/res/xml/preferences_debug.xml @@ -17,6 +17,10 @@ android:key="@string/p_debug_pro" android:title="@string/debug_pro"/> + + diff --git a/app/src/main/assets/licenses.json b/app/src/main/assets/licenses.json index bf2302e48..d8918268a 100644 --- a/app/src/main/assets/licenses.json +++ b/app/src/main/assets/licenses.json @@ -32,6 +32,22 @@ "version": "be6d515db8" } }, + { + "notice": null, + "copyrightHolder": "bitfire web engineering (Ricki Hirner, Bernhard Stockmann)", + "copyrightStatement": "Copyright © bitfire web engineering (Ricki Hirner, Bernhard Stockmann). All rights reserved.", + "license": "GNU General Public License, Version 3.0", + "licenseUrl": "https://www.gnu.org/licenses/gpl.txt", + "normalizedLicense": "gpl3", + "year": null, + "url": "", + "libraryName": "cert4android", + "artifactId": { + "name": "cert4android", + "group": "com.gitlab.bitfireAT", + "version": "1488e39a66" + } + }, { "notice": null, "copyrightHolder": "Android Open Source Project", @@ -45,7 +61,7 @@ "artifactId": { "name": "coordinatorlayout", "group": "androidx.coordinatorlayout", - "version": "1.1.0-rc01" + "version": "1.1.0" } }, { @@ -445,7 +461,7 @@ "artifactId": { "name": "annotation", "group": "androidx.annotation", - "version": "1.1.0" + "version": "1.0.0" } }, { @@ -925,7 +941,7 @@ "artifactId": { "name": "recyclerview", "group": "androidx.recyclerview", - "version": "1.1.0-rc01" + "version": "1.1.0" } }, { @@ -957,7 +973,7 @@ "artifactId": { "name": "collection", "group": "androidx.collection", - "version": "1.1.0" + "version": "1.0.0" } }, { @@ -1053,7 +1069,7 @@ "artifactId": { "name": "material", "group": "com.google.android.material", - "version": "1.1.0-beta02" + "version": "1.2.0-alpha03" } }, { @@ -1917,7 +1933,7 @@ "artifactId": { "name": "viewpager2", "group": "androidx.viewpager2", - "version": "1.0.0-rc01" + "version": "1.0.0" } }, { @@ -1983,6 +1999,166 @@ "group": "androidx.paging", "version": "2.1.1" } + }, + { + "notice": null, + "copyrightHolder": "JetBrains s.r.o.", + "copyrightStatement": "Copyright © JetBrains s.r.o. All rights reserved.", + "license": "The Apache Software License, Version 2.0", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "normalizedLicense": "apache2", + "year": null, + "url": "https://github.com/Kotlin/kotlinx.coroutines", + "libraryName": "kotlinx-coroutines-core-common", + "artifactId": { + "name": "kotlinx-coroutines-core-common", + "group": "org.jetbrains.kotlinx", + "version": "1.1.1" + } + }, + { + "notice": null, + "copyrightHolder": "Android Open Source Project", + "copyrightStatement": "Copyright © Android Open Source Project. All rights reserved.", + "license": "Apache 2", + "licenseUrl": "https://www.apache.org/licenses/LICENSE-2.0", + "normalizedLicense": "apache2", + "year": null, + "url": "https://conscrypt.org/", + "libraryName": "org.conscrypt:conscrypt-android", + "artifactId": { + "name": "conscrypt-android", + "group": "org.conscrypt", + "version": "2.2.1" + } + }, + { + "notice": null, + "copyrightHolder": "JetBrains s.r.o.", + "copyrightStatement": "Copyright © JetBrains s.r.o. All rights reserved.", + "license": "The Apache License, Version 2.0", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "normalizedLicense": "apache2", + "year": null, + "url": "https://kotlinlang.org/", + "libraryName": "org.jetbrains.kotlin:kotlin-stdlib-jdk8", + "artifactId": { + "name": "kotlin-stdlib-jdk8", + "group": "org.jetbrains.kotlin", + "version": "1.3.61" + } + }, + { + "notice": null, + "copyrightHolder": "JetBrains s.r.o.", + "copyrightStatement": "Copyright © JetBrains s.r.o. All rights reserved.", + "license": "The Apache Software License, Version 2.0", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "normalizedLicense": "apache2", + "year": null, + "url": "https://github.com/Kotlin/kotlinx.coroutines", + "libraryName": "kotlinx-coroutines-android", + "artifactId": { + "name": "kotlinx-coroutines-android", + "group": "org.jetbrains.kotlinx", + "version": "1.1.1" + } + }, + { + "notice": null, + "copyrightHolder": "Android Open Source Project", + "copyrightStatement": "Copyright © Android Open Source Project. All rights reserved.", + "license": "The Apache Software License, Version 2.0", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "normalizedLicense": "apache2", + "year": null, + "url": "", + "libraryName": "databinding-adapters", + "artifactId": { + "name": "databinding-adapters", + "group": "androidx.databinding", + "version": "3.5.3" + } + }, + { + "notice": null, + "copyrightHolder": "Android Open Source Project", + "copyrightStatement": "Copyright © Android Open Source Project. All rights reserved.", + "license": "The Apache Software License, Version 2.0", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "normalizedLicense": "apache2", + "year": null, + "url": "http://developer.android.com/tools/extras/support-library.html", + "libraryName": "Android Lifecycle ViewModel Kotlin Extensions", + "artifactId": { + "name": "lifecycle-viewmodel-ktx", + "group": "androidx.lifecycle", + "version": "2.1.0" + } + }, + { + "notice": null, + "copyrightHolder": "Android Open Source Project", + "copyrightStatement": "Copyright © Android Open Source Project. All rights reserved.", + "license": "The Apache Software License, Version 2.0", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "normalizedLicense": "apache2", + "year": null, + "url": "https://developer.android.com/jetpack/androidx", + "libraryName": "Experimental annotation", + "artifactId": { + "name": "annotation-experimental", + "group": "androidx.annotation", + "version": "1.0.0" + } + }, + { + "notice": null, + "copyrightHolder": "JetBrains s.r.o.", + "copyrightStatement": "Copyright © JetBrains s.r.o. All rights reserved.", + "license": "The Apache Software License, Version 2.0", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "normalizedLicense": "apache2", + "year": null, + "url": "https://github.com/Kotlin/kotlinx.coroutines", + "libraryName": "kotlinx-coroutines-core", + "artifactId": { + "name": "kotlinx-coroutines-core", + "group": "org.jetbrains.kotlinx", + "version": "1.1.1" + } + }, + { + "notice": null, + "copyrightHolder": "Android Open Source Project", + "copyrightStatement": "Copyright © Android Open Source Project. All rights reserved.", + "license": "The Apache Software License, Version 2.0", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "normalizedLicense": "apache2", + "year": null, + "url": "https://developer.android.com/studio", + "libraryName": "Data Binding Base Library", + "artifactId": { + "name": "databinding-common", + "group": "androidx.databinding", + "version": "3.5.3" + } + }, + { + "notice": null, + "copyrightHolder": "Android Open Source Project", + "copyrightStatement": "Copyright © Android Open Source Project. All rights reserved.", + "license": "The Apache Software License, Version 2.0", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "normalizedLicense": "apache2", + "year": null, + "url": "", + "libraryName": "databinding-runtime", + "artifactId": { + "name": "databinding-runtime", + "group": "androidx.databinding", + "version": "3.5.3" + } } ] } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/activities/AddCaldavAccountViewModel.java b/app/src/main/java/org/tasks/activities/AddCaldavAccountViewModel.java index 3b58e6ede..3706224fa 100644 --- a/app/src/main/java/org/tasks/activities/AddCaldavAccountViewModel.java +++ b/app/src/main/java/org/tasks/activities/AddCaldavAccountViewModel.java @@ -16,7 +16,7 @@ public class AddCaldavAccountViewModel extends CompletableViewModel { run( () -> { playServices.updateSecurityProvider(context); - return client.forUrl(url, username, password).getHomeSet(); + return client.setForeground().forUrl(url, username, password).getHomeSet(); }); } } diff --git a/app/src/main/java/org/tasks/caldav/CaldavClient.java b/app/src/main/java/org/tasks/caldav/CaldavClient.java index 0d95e8498..a548b51bf 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavClient.java +++ b/app/src/main/java/org/tasks/caldav/CaldavClient.java @@ -6,6 +6,9 @@ import static at.bitfire.dav4jvm.XmlUtils.NS_CARDDAV; import static at.bitfire.dav4jvm.XmlUtils.NS_WEBDAV; import static java.util.Arrays.asList; +import android.content.Context; +import at.bitfire.cert4android.CustomCertManager; +import at.bitfire.cert4android.CustomCertManager.CustomHostnameVerifier; import at.bitfire.dav4jvm.BasicDigestAuthHandler; import at.bitfire.dav4jvm.DavResource; import at.bitfire.dav4jvm.Property.Name; @@ -23,17 +26,23 @@ import at.bitfire.dav4jvm.property.SupportedCalendarComponentSet; import com.todoroo.astrid.helper.UUIDHelper; import java.io.IOException; import java.io.StringWriter; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import javax.inject.Inject; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.OkHttpClient.Builder; +import okhttp3.internal.tls.OkHostnameVerifier; import org.tasks.DebugNetworkInterceptor; import org.tasks.R; import org.tasks.data.CaldavAccount; import org.tasks.data.CaldavCalendar; +import org.tasks.injection.ForApplication; import org.tasks.preferences.Preferences; import org.tasks.security.Encryption; import org.tasks.ui.DisplayableException; @@ -49,10 +58,16 @@ public class CaldavClient { private final DebugNetworkInterceptor interceptor; private final OkHttpClient httpClient; private final HttpUrl httpUrl; + private final Context context; + private boolean foreground; @Inject public CaldavClient( - Encryption encryption, Preferences preferences, DebugNetworkInterceptor interceptor) { + @ForApplication Context context, + Encryption encryption, + Preferences preferences, + DebugNetworkInterceptor interceptor) { + this.context = context; this.encryption = encryption; this.preferences = preferences; this.interceptor = interceptor; @@ -61,16 +76,26 @@ public class CaldavClient { } private CaldavClient( + Context context, Encryption encryption, Preferences preferences, DebugNetworkInterceptor interceptor, String url, String username, - String password) { + String password, + boolean foreground) throws NoSuchAlgorithmException, KeyManagementException { + this.context = context; this.encryption = encryption; this.preferences = preferences; this.interceptor = interceptor; + CustomCertManager customCertManager = new CustomCertManager(context); + customCertManager.setAppInForeground(foreground); + CustomHostnameVerifier hostnameVerifier = + customCertManager.hostnameVerifier(OkHostnameVerifier.INSTANCE); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] { customCertManager }, null); + BasicDigestAuthHandler basicDigestAuthHandler = new BasicDigestAuthHandler(null, username, password); Builder builder = @@ -81,6 +106,8 @@ public class CaldavClient { .cookieJar(new MemoryCookieStore()) .followRedirects(false) .followSslRedirects(true) + .sslSocketFactory(sslContext.getSocketFactory(), customCertManager) + .hostnameVerifier(hostnameVerifier) .readTimeout(30, TimeUnit.SECONDS); if (preferences.isFlipperEnabled()) { interceptor.add(builder); @@ -89,16 +116,20 @@ public class CaldavClient { httpUrl = HttpUrl.parse(url); } - public CaldavClient forAccount(CaldavAccount account) { + public CaldavClient forAccount(CaldavAccount account) + throws NoSuchAlgorithmException, KeyManagementException { return forUrl(account.getUrl(), account.getUsername(), account.getPassword(encryption)); } - public CaldavClient forCalendar(CaldavAccount account, CaldavCalendar calendar) { + public CaldavClient forCalendar(CaldavAccount account, CaldavCalendar calendar) + throws NoSuchAlgorithmException, KeyManagementException { return forUrl(calendar.getUrl(), account.getUsername(), account.getPassword(encryption)); } - public CaldavClient forUrl(String url, String username, String password) { - return new CaldavClient(encryption, preferences, interceptor, url, username, password); + public CaldavClient forUrl(String url, String username, String password) + throws KeyManagementException, NoSuchAlgorithmException { + return new CaldavClient( + context, encryption, preferences, interceptor, url, username, password, foreground); } private String tryFindPrincipal() throws DavException, IOException { @@ -234,4 +265,9 @@ public class CaldavClient { OkHttpClient getHttpClient() { return httpClient; } + + public CaldavClient setForeground() { + foreground = true; + return this; + } } diff --git a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.java b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.java index 16a2ecea7..ab6dfe93e 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.java +++ b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.java @@ -39,6 +39,8 @@ import java.io.IOException; import java.net.ConnectException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -125,7 +127,9 @@ public class CaldavSynchronizer { | ConnectException | UnknownHostException | UnauthorizedException - | ServiceUnavailableException e) { + | ServiceUnavailableException + | KeyManagementException + | NoSuchAlgorithmException e) { setError(account, e.getMessage()); } catch (IOException | DavException e) { setError(account, e.getMessage()); @@ -135,7 +139,8 @@ public class CaldavSynchronizer { } } - private void synchronize(CaldavAccount account) throws IOException, DavException { + private void synchronize(CaldavAccount account) + throws IOException, DavException, KeyManagementException, NoSuchAlgorithmException { CaldavClient caldavClient = client.forAccount(account); List resources = caldavClient.getCalendars(); Set urls = newHashSet(transform(resources, c -> c.getHref().toString()));