Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 30 additions & 20 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ android {


packagingOptions {
// Store native libraries uncompressed and page-aligned so the APK satisfies the
// 16 KB page-size requirement (Android 15+). Note: this only fixes APK-level
// alignment; each .so must also be linked with 16 KB-aligned LOAD segments.
jniLibs {
useLegacyPackaging = false
}
resources {
excludes += ['LICENSE', 'META-INF/LICENSE.txt', 'META-INF/NOTICE.txt']
}
Expand Down Expand Up @@ -127,29 +133,32 @@ dependencies {
implementation "com.etesync:journalmanager:1.1.1"
def etebaseVersion = '2.3.2'
implementation "com.etebase:client:$etebaseVersion"
// conscrypt is pulled transitively by etebase at 2.5.0, whose native lib is not
// 16 KB-page aligned; force 2.5.3 whose prebuilt .so ships 16 KB-aligned LOAD segments.
implementation 'org.conscrypt:conscrypt-android:2.5.3'

// ACRA kept at 5.7.0 in Phase 1: newer 5.x is built with Kotlin >= 1.9 and its
// metadata would be rejected by the Kotlin 1.8.21 compiler. Bump in Phase 2 with Kotlin.
def acraVersion = '5.7.0'
implementation "ch.acra:acra-http:$acraVersion"
implementation "ch.acra:acra-dialog:$acraVersion"
// legacy-* artifacts only ever shipped at 1.0.0; core is bumped explicitly below.
def supportVersion = '1.0.0'
implementation "androidx.legacy:legacy-support-core-ui:$supportVersion"
implementation "androidx.core:core:$supportVersion"
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.core:core:1.12.0"
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.cardview:cardview:1.0.0"
// KTX extensions
implementation "androidx.core:core-ktx:1.3.1"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
// KTX extensions — capped at versions built with Kotlin <= 1.8 (Phase 1 keeps Kotlin 1.8.21).
implementation "androidx.core:core-ktx:1.12.0"
implementation "androidx.fragment:fragment-ktx:1.6.2"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0"


implementation 'com.google.android.material:material:1.2.0-beta01'
implementation 'com.google.android.material:material:1.12.0'
implementation "androidx.legacy:legacy-preference-v14:$supportVersion"
implementation 'com.github.yukuku:ambilwarna:2.0.1'
implementation ('com.github.worker8:tourguide:1.0.18-SNAPSHOT@aar') {
transitive = true
}

def requeryVersion = '1.6.0'
implementation "io.requery:requery:$requeryVersion"
Expand All @@ -158,27 +167,28 @@ dependencies {
kapt "io.requery:requery-processor:$requeryVersion"
implementation 'com.google.code.findbugs:jsr305:3.0.2'

def okhttp3Version = "4.8.1"
// okhttp stays on 4.x for JDK8/minSdk21 compatibility; 4.12.0 fixes CVE-2021-0341.
def okhttp3Version = "4.12.0"
implementation "com.squareup.okhttp3:okhttp:$okhttp3Version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp3Version"

implementation 'com.google.code.gson:gson:1.7.2'
implementation 'org.apache.commons:commons-collections4:4.1'
implementation 'org.apache.commons:commons-lang3:3.8.1'
implementation 'com.google.code.gson:gson:2.11.0'
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'org.apache.commons:commons-lang3:3.14.0'

implementation 'com.github.cachapa:ExpandableLayout:2.9.2'
implementation project(':cert4android')
implementation project(':ical4android')
implementation project(':vcard4android')
// for tests
androidTestImplementation('androidx.test:runner:1.1.0-alpha4') {
androidTestImplementation('androidx.test:runner:1.5.2') {
exclude group: 'com.android.support', module: 'support-annotations'
}
androidTestImplementation('androidx.test:rules:1.1.0-alpha4') {
androidTestImplementation('androidx.test:rules:1.5.0') {
exclude group: 'com.android.support', module: 'support-annotations'
}
androidTestImplementation 'junit:junit:4.12'
androidTestImplementation 'junit:junit:4.13.2'
androidTestImplementation "com.squareup.okhttp3:mockwebserver:$okhttp3Version"
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13.2'
testImplementation "com.squareup.okhttp3:mockwebserver:$okhttp3Version"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.etesync.syncadapter.syncadapter

import android.content.ComponentName
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

/**
* Guards the exported surface of the sync and account services, as declared in the *merged*
* AndroidManifest.xml.
*
* Sync adapter services must stay unexported: the system server binds them either way (it is
* exempt from the exported check), while exporting them hands the ISyncAdapter Binder to any
* local app, which can then crash the sync process with a malformed transaction.
* See https://github.com/etesync/android/issues/295.
*/
class ExportedComponentsTest {

private val context: Context
get() = InstrumentationRegistry.getInstrumentation().targetContext

private fun isExported(cls: Class<*>) =
context.packageManager.getServiceInfo(ComponentName(context, cls), 0).exported

@Test
fun syncAdapterServicesAreNotExported() {
for (cls in arrayOf(
CalendarsSyncAdapterService::class.java,
ContactsSyncAdapterService::class.java,
AddressBooksSyncAdapterService::class.java,
TasksSyncAdapterService::class.java,
TasksOrgSyncAdapterService::class.java)) {
assertFalse("${cls.simpleName} must not be exported: any local app could then bind it " +
"and crash the sync process (etesync/android#295)", isExported(cls))
}
}

@Test
fun accountAuthenticatorServiceIsNotExported() {
assertFalse("AccountAuthenticatorService is bound by the system and must not be exported",
isExported(AccountAuthenticatorService::class.java))
}

@Test
fun nullAuthenticatorServiceIsExported() {
// Since Android 11 this one has to stay exported, otherwise Google Contacts doesn't show
// the address book accounts.
assertTrue("NullAuthenticatorService must stay exported so that other apps can see the " +
"address book accounts", isExported(NullAuthenticatorService::class.java))
}
}
27 changes: 15 additions & 12 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<manifest package="com.etesync.syncadapter"
xmlns:android="http://schemas.android.com/apk/res/android"
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly">
<queries>
Expand Down Expand Up @@ -90,11 +89,19 @@
android:resource="@xml/account_authenticator"/>
</service>

<!--
Sync adapter services must NOT be exported. They are bound by the system server
(SyncManager), which is exempt from the exported check, so the sync framework keeps
working without it. Exporting them hands the ISyncAdapter Binder to any local app,
which can then crash the sync process with a malformed transaction (NPE in
AbstractThreadedSyncAdapter.handleOnUnsyncableAccount).
See https://github.com/etesync/android/issues/295.
-->

<!-- Normal account -->
<service
android:name=".syncadapter.CalendarsSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
android:exported="false">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
Expand All @@ -105,8 +112,7 @@
</service>
<service
android:name=".syncadapter.TasksSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
android:exported="false">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
Expand All @@ -118,8 +124,7 @@

<service
android:name=".syncadapter.TasksOrgSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
android:exported="false">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
Expand Down Expand Up @@ -150,8 +155,7 @@
android:multiprocess="false"/>
<service
android:name=".syncadapter.AddressBooksSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
android:exported="false">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
Expand All @@ -163,8 +167,7 @@

<service
android:name=".syncadapter.ContactsSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
android:exported="false">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/com/etesync/syncadapter/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ class App : Application() {
tasksFilter.addAction(Intent.ACTION_PACKAGE_ADDED)
tasksFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED)
tasksFilter.addDataScheme("package")
registerReceiver(PackageChangedReceiver(), tasksFilter)
// Explicit export flag (required semantics on Android 14+); this receiver only needs
// the app's own process, so mark it not-exported.
ContextCompat.registerReceiver(this@App, PackageChangedReceiver(), tasksFilter, ContextCompat.RECEIVER_NOT_EXPORTED)

TASK_PROVIDERS.forEach {
// check whether a tasks app is currently installed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,14 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
val collections: List<CachedCollection>
synchronized(etebaseLocalCache) {
val httpClient = HttpClient.Builder(context, settings).setForeground(false).build()
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
val colMgr = etebase.collectionManager
// Close the client so cert4android's CustomCertManager unbinds its service
// (otherwise the ServiceConnection leaks on every address-book sync).
HttpClient.Builder(context, settings).setForeground(false).build().use { httpClient ->
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
val colMgr = etebase.collectionManager

collections = etebaseLocalCache.collectionList(colMgr).filter { it.collectionType == Constants.ETEBASE_TYPE_ADDRESS_BOOK }
collections = etebaseLocalCache.collectionList(colMgr).filter { it.collectionType == Constants.ETEBASE_TYPE_ADDRESS_BOOK }
}
}

for (collection in collections) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,14 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
val collections: List<CachedCollection>
synchronized(etebaseLocalCache) {
val httpClient = HttpClient.Builder(context, settings).setForeground(false).build()
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
val colMgr = etebase.collectionManager
// Close the client so cert4android's CustomCertManager unbinds its service
// (otherwise the ServiceConnection leaks on every calendar sync).
HttpClient.Builder(context, settings).setForeground(false).build().use { httpClient ->
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
val colMgr = etebase.collectionManager

collections = etebaseLocalCache.collectionList(colMgr).filter { it.collectionType == Constants.ETEBASE_TYPE_CALENDAR }
collections = etebaseLocalCache.collectionList(colMgr).filter { it.collectionType == Constants.ETEBASE_TYPE_CALENDAR }
}
}

for (collection in collections) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,20 +313,24 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
return null
}

val resourceClient = HttpClient.Builder(context).setForeground(false).build().okHttpClient
// Close the client so cert4android's CustomCertManager unbinds its service
// (otherwise the ServiceConnection leaks for every external resource fetched).
HttpClient.Builder(context).setForeground(false).build().use { httpClient ->
val resourceClient = httpClient.okHttpClient

try {
val response = resourceClient.newCall(Request.Builder()
.get()
.url(httpUrl)
.build()).execute()

val body = response.body
if (body != null) {
return body.bytes()
try {
val response = resourceClient.newCall(Request.Builder()
.get()
.url(httpUrl)
.build()).execute()

val body = response.body
if (body != null) {
return body.bytes()
}
} catch (e: IOException) {
Logger.log.log(Level.SEVERE, "Couldn't download external resource", e)
}
} catch (e: IOException) {
Logger.log.log(Level.SEVERE, "Couldn't download external resource", e)
}

return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,14 @@ class TasksSyncAdapterService: SyncAdapterService() {
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
val collections: List<CachedCollection>
synchronized(etebaseLocalCache) {
val httpClient = HttpClient.Builder(context, settings).setForeground(false).build()
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
val colMgr = etebase.collectionManager
// Close the client so cert4android's CustomCertManager unbinds its service
// (otherwise the ServiceConnection leaks on every task-list sync).
HttpClient.Builder(context, settings).setForeground(false).build().use { httpClient ->
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
val colMgr = etebase.collectionManager

collections = etebaseLocalCache.collectionList(colMgr).filter { it.collectionType == Constants.ETEBASE_TYPE_TASKS }
collections = etebaseLocalCache.collectionList(colMgr).filter { it.collectionType == Constants.ETEBASE_TYPE_TASKS }
}
}

for (collection in collections) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ import com.etesync.syncadapter.ui.etebase.CollectionActivity
import com.etesync.syncadapter.ui.etebase.InvitationsActivity
import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment
import com.etesync.syncadapter.utils.HintManager
import com.etesync.syncadapter.utils.ShowcaseBuilder
import com.etesync.syncadapter.utils.packageInstalled
import com.google.android.material.snackbar.Snackbar
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
Expand All @@ -62,7 +61,6 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import tourguide.tourguide.ToolTip
import java.util.logging.Level

class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener, Refreshable {
Expand Down Expand Up @@ -160,13 +158,6 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
}
}

if (!HintManager.getHintSeen(this, HINT_VIEW_COLLECTION)) {
ShowcaseBuilder.getBuilder(this)
.setToolTip(ToolTip().setTitle(getString(R.string.tourguide_title)).setDescription(getString(R.string.account_showcase_view_collection)))
.playOn(tbCardDAV)
HintManager.setHintSeen(this, HINT_VIEW_COLLECTION, true)
}

if (settings.isLegacy) {
if (!SetupUserInfoFragment.hasUserInfo(this, account)) {
SetupUserInfoFragment.newInstance(account).show(supportFragmentManager, null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,9 @@ import com.etesync.syncadapter.Constants.serviceUrl
import com.etesync.syncadapter.R
import com.etesync.syncadapter.ui.setup.LoginActivity
import com.etesync.syncadapter.utils.HintManager
import com.etesync.syncadapter.utils.ShowcaseBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import tourguide.tourguide.ToolTip

class AccountsActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener, SyncStatusObserver {

Expand Down Expand Up @@ -72,13 +70,6 @@ class AccountsActivity : BaseActivity(), NavigationView.OnNavigationItemSelected
}

PermissionsActivity.requestAllPermissions(this)

if (!HintManager.getHintSeen(this, HINT_ACCOUNT_ADD)) {
ShowcaseBuilder.getBuilder(this)
.setToolTip(ToolTip().setTitle(getString(R.string.tourguide_title)).setDescription(getString(R.string.accounts_showcase_add)).setGravity(Gravity.TOP or Gravity.LEFT))
.playOn(fab)
HintManager.setHintSeen(this, HINT_ACCOUNT_ADD, true)
}
}

override fun onResume() {
Expand Down
Loading