diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 66670365ae6a3ec95f704aae8f240480a0ff4455..7acc6ca447f114c941df1b23aa0f6601d99ffd81 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -336,9 +336,9 @@
android:resource="@xml/contacts" />
-
+
@@ -346,11 +346,11 @@
+ android:resource="@xml/yahoo_account_authenticator" />
@@ -360,11 +360,11 @@
+ android:resource="@xml/yahoo_sync_calendars" />
@@ -374,12 +374,12 @@
+ android:resource="@xml/yahoo_sync_tasks" />
-
+
@@ -387,11 +387,11 @@
+ android:resource="@xml/account_authenticator_yahoo_address_book" />
@@ -401,11 +401,11 @@
+ android:resource="@xml/yahoo_sync_address_books" />
@@ -415,14 +415,14 @@
+ android:resource="@xml/yahoo_sync_contacts" />
@@ -432,7 +432,65 @@
+ android:resource="@xml/yahoo_sync_email" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooAccountAuthenticatorService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooAccountAuthenticatorService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..763f49ff365bbe47a9dc02d941280510cf9c0d56
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooAccountAuthenticatorService.kt
@@ -0,0 +1,159 @@
+/*
+ * 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 foundation.e.accountmanager.syncadapter
+
+import android.accounts.*
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.database.DatabaseUtils
+import android.os.Bundle
+import androidx.annotation.WorkerThread
+import foundation.e.accountmanager.R
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.model.AppDatabase
+import foundation.e.accountmanager.resource.LocalAddressBook
+import foundation.e.accountmanager.ui.setup.LoginActivity
+import java.util.*
+import java.util.logging.Level
+import kotlin.concurrent.thread
+import android.accounts.AccountManager
+import foundation.e.accountmanager.settings.AccountSettings
+import net.openid.appauth.AuthState
+import net.openid.appauth.AuthorizationService
+
+/**
+ * Account authenticator for the Yahoo account type.
+ *
+ * Gets started when a Yahoo account is removed, too, so it also watches for account removals
+ * and contains the corresponding cleanup code.
+ */
+class YahooAccountAuthenticatorService : Service(), OnAccountsUpdateListener {
+
+ companion object {
+
+ fun cleanupAccounts(context: Context) {
+ Logger.log.info("Cleaning up orphaned accounts")
+
+ val accountManager = AccountManager.get(context)
+
+ val accountNames = HashSet()
+ val accounts = ArrayList()
+ accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)).forEach { accounts.add(it) }
+ accountManager.getAccountsByType(context.getString(R.string.google_account_type)).forEach { accounts.add(it) }
+ accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { accounts.add(it) }
+ accountManager.getAccountsByType(context.getString(R.string.yahoo_account_type)).forEach { accounts.add(it) }
+ for (account in accounts.toTypedArray())
+ accountNames += account.name
+
+ // delete orphaned address book accounts
+ val addressBookAccounts = ArrayList()
+ accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book)).forEach { addressBookAccounts.add(it) }
+ accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book)).forEach { addressBookAccounts.add(it) }
+ accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).forEach { addressBookAccounts.add(it) }
+ accountManager.getAccountsByType(context.getString(R.string.account_type_yahoo_address_book)).forEach { addressBookAccounts.add(it) }
+ addressBookAccounts.map { LocalAddressBook(context, it, null) }
+ .forEach {
+ try {
+ if (!accountNames.contains(it.mainAccount.name))
+ it.delete()
+ } catch(e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e)
+ }
+ }
+
+ // delete orphaned services in DB
+ val db = AppDatabase.getInstance(context)
+ val serviceDao = db.serviceDao()
+ if (accountNames.isEmpty())
+ serviceDao.deleteAll()
+ else
+ serviceDao.deleteExceptAccounts(accountNames.toTypedArray())
+ }
+
+ }
+
+ 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?) {
+ thread {
+ cleanupAccounts(this)
+ }
+ }
+
+
+ private class AccountAuthenticator(
+ val context: Context
+ ) : AbstractAccountAuthenticator(context) {
+
+ override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?): Bundle {
+ val intent = Intent(context, LoginActivity::class.java)
+ intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
+ intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_YAHOO)
+ val bundle = Bundle(1)
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent)
+ return bundle
+ }
+
+ override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = null
+ override fun getAuthTokenLabel(p0: String?) = null
+ override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?) = null
+ override fun updateCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
+ override fun getAuthToken(response: AccountAuthenticatorResponse?, account: Account?, authTokenType: String?, options: Bundle?): Bundle {
+ val accountManager = AccountManager.get(context)
+ val authState = AuthState.jsonDeserialize(accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE))
+
+ if (authState != null) {
+ if (authState.needsTokenRefresh) {
+ val tokenRequest = authState.createTokenRefreshRequest()
+
+ AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex ->
+ authState.update(tokenResponse, ex)
+ accountManager.setUserData(account, AccountSettings.KEY_AUTH_STATE, authState.jsonSerializeString())
+ val result = Bundle()
+ result.putString(AccountManager.KEY_ACCOUNT_NAME, account!!.name)
+ result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type)
+ result.putString(AccountManager.KEY_AUTHTOKEN, authState.accessToken)
+ response?.onResult(result)
+ }
+ }
+ else {
+ val result = Bundle()
+ result.putString(AccountManager.KEY_ACCOUNT_NAME, account!!.name)
+ result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type)
+ result.putString(AccountManager.KEY_AUTHTOKEN, authState.accessToken)
+ return result
+ }
+ }
+
+ val result = Bundle()
+ result.putInt(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION)
+ return result
+ }
+ override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?) = null
+
+ }
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooAddressBooksSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooAddressBooksSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..16b148abdf3220c1da75e1027b2b115f569d6836
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooAddressBooksSyncAdapterService.kt
@@ -0,0 +1,129 @@
+/*
+ * 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 foundation.e.accountmanager.syncadapter
+
+import android.Manifest
+import android.accounts.Account
+import android.content.*
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.provider.ContactsContract
+import androidx.core.content.ContextCompat
+import foundation.e.accountmanager.closeCompat
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.model.AppDatabase
+import foundation.e.accountmanager.model.Collection
+import foundation.e.accountmanager.model.Service
+import foundation.e.accountmanager.resource.LocalAddressBook
+import foundation.e.accountmanager.settings.AccountSettings
+import foundation.e.accountmanager.ui.account.AccountActivity
+import okhttp3.HttpUrl
+import java.util.logging.Level
+
+class YahooAddressBooksSyncAdapterService : SyncAdapterService() {
+
+ override fun syncAdapter() = AddressBooksSyncAdapter(this)
+
+
+ class AddressBooksSyncAdapter(
+ context: Context
+ ) : SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ try {
+ val accountSettings = AccountSettings(context, account)
+
+ /* don't run sync if
+ - sync conditions (e.g. "sync only in WiFi") are not met AND
+ - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
+ */
+ if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
+ return
+
+ if (updateLocalAddressBooks(account, syncResult))
+ for (addressBookAccount in LocalAddressBook.findAll(context, null, account).map { it.account }) {
+ Logger.log.log(Level.INFO, "Running sync for address book", addressBookAccount)
+ val syncExtras = Bundle(extras)
+ syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true)
+ syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true)
+ ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras)
+ }
+ } catch (e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't sync address books", e)
+ }
+
+ Logger.log.info("Address book sync complete")
+ }
+
+ private fun updateLocalAddressBooks(account: Account, syncResult: SyncResult): Boolean {
+ val db = AppDatabase.getInstance(context)
+ val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CARDDAV)
+
+ val remoteAddressBooks = mutableMapOf()
+ if (service != null)
+ for (collection in db.collectionDao().getByServiceAndSync(service.id))
+ remoteAddressBooks[collection.url] = collection
+
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+ if (remoteAddressBooks.isEmpty())
+ Logger.log.info("No contacts permission, but no address book selected for synchronization")
+ else {
+ // no contacts permission, but address books should be synchronized -> show notification
+ val intent = Intent(context, AccountActivity::class.java)
+ intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ notifyPermissions(intent)
+ }
+ return false
+ }
+
+ val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
+ try {
+ if (contactsProvider == null) {
+ Logger.log.severe("Couldn't access contacts provider")
+ syncResult.databaseError = true
+ return false
+ }
+
+ // delete/update local address books
+ for (addressBook in LocalAddressBook.findAll(context, contactsProvider, account)) {
+ val url = HttpUrl.parse(addressBook.url)!!
+ val info = remoteAddressBooks[url]
+ if (info == null) {
+ Logger.log.log(Level.INFO, "Deleting obsolete local address book", url)
+ addressBook.delete()
+ } else {
+ // remote CollectionInfo found for this local collection, update data
+ try {
+ Logger.log.log(Level.FINE, "Updating local address book $url", info)
+ addressBook.update(info)
+ } catch (e: Exception) {
+ Logger.log.log(Level.WARNING, "Couldn't rename address book account", e)
+ }
+ // we already have a local address book for this remote collection, don't take into consideration anymore
+ remoteAddressBooks -= url
+ }
+ }
+
+ // create new local address books
+ for ((_, info) in remoteAddressBooks) {
+ Logger.log.log(Level.INFO, "Adding local address book", info)
+ LocalAddressBook.create(context, contactsProvider, account, info)
+ }
+ } finally {
+ contactsProvider?.closeCompat()
+ }
+
+ return true
+ }
+
+ }
+
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooCalendarsSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooCalendarsSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..540e977396718e59463e7e4ecdf8c51af0c08597
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooCalendarsSyncAdapterService.kt
@@ -0,0 +1,137 @@
+/*
+ * 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 foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.content.ContentProviderClient
+import android.content.ContentResolver
+import android.content.Context
+import android.content.SyncResult
+import android.os.Bundle
+import android.os.AsyncTask
+import android.provider.CalendarContract
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.model.AppDatabase
+import foundation.e.accountmanager.model.Collection
+import foundation.e.accountmanager.model.Credentials
+import foundation.e.accountmanager.model.Service
+import foundation.e.accountmanager.resource.LocalCalendar
+import foundation.e.accountmanager.settings.AccountSettings
+import foundation.e.ical4android.AndroidCalendar
+import net.openid.appauth.AuthorizationService
+import okhttp3.HttpUrl
+import java.util.logging.Level
+
+class YahooCalendarsSyncAdapterService: SyncAdapterService() {
+
+ override fun syncAdapter() = CalendarsSyncAdapter(this)
+
+
+ class CalendarsSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ try {
+ val accountSettings = AccountSettings(context, account)
+
+ /* don't run sync if
+ - sync conditions (e.g. "sync only in WiFi") are not met AND
+ - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
+ */
+ if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
+ return
+
+ if (accountSettings.getEventColors())
+ AndroidCalendar.insertColors(provider, account)
+ else
+ AndroidCalendar.removeColors(provider, account)
+
+ updateLocalCalendars(provider, account, accountSettings)
+
+ val priorityCalendars = priorityCollections(extras)
+ val calendars = AndroidCalendar
+ .find(account, provider, LocalCalendar.Factory, "${CalendarContract.Calendars.SYNC_EVENTS}!=0", null)
+ .sortedByDescending { priorityCalendars.contains(it.id) }
+ for (calendar in calendars) {
+ Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
+ CalendarSyncManager(context, account, accountSettings, extras, authority, syncResult, calendar).use {
+ val authState = accountSettings.credentials().authState
+ if (authState != null)
+ {
+ if (authState.needsTokenRefresh)
+ {
+ val tokenRequest = authState.createTokenRefreshRequest()
+
+ AuthorizationService(context).performTokenRequest(tokenRequest,
+ AuthorizationService.TokenResponseCallback { tokenResponse, ex ->
+ authState.update(tokenResponse, ex)
+ accountSettings.credentials(Credentials(account.name, null, authState, null))
+ it.accountSettings.credentials(Credentials(it.account.name, null, authState, null))
+ object : AsyncTask() {
+ override fun doInBackground(vararg params: Void): Void? {
+ it.performSync()
+ return null
+ }
+ }.execute()
+ })
+ }
+ else {
+ it.performSync()
+ }
+ }
+ else {
+ it.performSync()
+ }
+ }
+ }
+ } catch(e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't sync calendars", e)
+ }
+ Logger.log.info("Calendar sync complete")
+ }
+
+ private fun updateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
+ val db = AppDatabase.getInstance(context)
+ val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
+
+ val remoteCalendars = mutableMapOf()
+ if (service != null)
+ for (collection in db.collectionDao().getSyncCalendars(service.id)) {
+ remoteCalendars[collection.url] = collection
+ }
+
+ // delete/update local calendars
+ val updateColors = settings.getManageCalendarColors()
+ for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null))
+ calendar.name?.let {
+ val url = HttpUrl.parse(it)!!
+ val info = remoteCalendars[url]
+ if (info == null) {
+ Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url)
+ calendar.delete()
+ } else {
+ // remote CollectionInfo found for this local collection, update data
+ Logger.log.log(Level.FINE, "Updating local calendar $url", info)
+ calendar.update(info, updateColors)
+ // we already have a local calendar for this remote collection, don't take into consideration anymore
+ remoteCalendars -= url
+ }
+ }
+
+ // create new local calendars
+ for ((_, info) in remoteCalendars) {
+ Logger.log.log(Level.INFO, "Adding local calendar", info)
+ LocalCalendar.create(account, provider, info)
+ }
+ }
+
+ }
+
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooContactsSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooContactsSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7e677399fabc77ef15554f3480ebae8c561039bf
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooContactsSyncAdapterService.kt
@@ -0,0 +1,79 @@
+/*
+ * 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 foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.content.ContentProviderClient
+import android.content.ContentResolver
+import android.content.Context
+import android.content.SyncResult
+import android.os.Bundle
+import android.provider.ContactsContract
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.resource.LocalAddressBook
+import foundation.e.accountmanager.settings.AccountSettings
+import java.util.logging.Level
+
+class YahooContactsSyncAdapterService: SyncAdapterService() {
+
+ companion object {
+ const val PREVIOUS_GROUP_METHOD = "previous_group_method"
+ }
+
+ override fun syncAdapter() = ContactsSyncAdapter(this)
+
+
+ class ContactsSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ try {
+ val addressBook = LocalAddressBook(context, account, provider)
+ val accountSettings = AccountSettings(context, addressBook.mainAccount)
+
+ // handle group method change
+ val groupMethod = accountSettings.getGroupMethod().name
+ accountSettings.accountManager.getUserData(account, PREVIOUS_GROUP_METHOD)?.let { previousGroupMethod ->
+ if (previousGroupMethod != groupMethod) {
+ Logger.log.info("Group method changed, deleting all local contacts/groups")
+
+ // delete all local contacts and groups so that they will be downloaded again
+ provider.delete(addressBook.syncAdapterURI(ContactsContract.RawContacts.CONTENT_URI), null, null)
+ provider.delete(addressBook.syncAdapterURI(ContactsContract.Groups.CONTENT_URI), null, null)
+
+ // reset sync state
+ addressBook.syncState = null
+ }
+ }
+ accountSettings.accountManager.setUserData(account, PREVIOUS_GROUP_METHOD, groupMethod)
+
+ /* don't run sync if
+ - sync conditions (e.g. "sync only in WiFi") are not met AND
+ - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
+ */
+ if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
+ return
+
+ Logger.log.info("Synchronizing address book: ${addressBook.url}")
+ Logger.log.info("Taking settings from: ${addressBook.mainAccount}")
+
+ ContactsSyncManager(context, account, accountSettings, extras, authority, syncResult, provider, addressBook).use {
+ it.performSync()
+ }
+ } catch(e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't sync contacts", e)
+ }
+ Logger.log.info("Contacts sync complete")
+ }
+
+ }
+
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooEmailSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooEmailSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a6faf12643445e3573c469c6770816ff0ad4c10e
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooEmailSyncAdapterService.kt
@@ -0,0 +1,29 @@
+/*
+ * 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 foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.content.*
+import android.os.Bundle
+
+class YahooEmailSyncAdapterService : SyncAdapterService() {
+
+ override fun syncAdapter() = CalendarsSyncAdapter(this)
+
+
+ class CalendarsSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ // Unused
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooNullAuthenticatorService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooNullAuthenticatorService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e8f1ea281802abab35f3b41ae2cc69c1aa60073a
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooNullAuthenticatorService.kt
@@ -0,0 +1,54 @@
+/*
+ * 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 foundation.e.accountmanager.syncadapter
+
+import android.accounts.AbstractAccountAuthenticator
+import android.accounts.Account
+import android.accounts.AccountAuthenticatorResponse
+import android.accounts.AccountManager
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import foundation.e.accountmanager.ui.AccountsActivity
+
+class YahooNullAuthenticatorService: Service() {
+
+ private lateinit var accountAuthenticator: AccountAuthenticator
+
+ override fun onCreate() {
+ accountAuthenticator = AccountAuthenticator(this)
+ }
+
+ override fun onBind(intent: Intent?) =
+ accountAuthenticator.iBinder.takeIf { intent?.action == AccountManager.ACTION_AUTHENTICATOR_INTENT }
+
+
+ private class AccountAuthenticator(
+ val context: Context
+ ): AbstractAccountAuthenticator(context) {
+
+ override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?): Bundle {
+ val intent = Intent(context, AccountsActivity::class.java)
+ intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
+ val bundle = Bundle(1)
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent)
+ return bundle
+ }
+
+ override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = null
+ override fun getAuthTokenLabel(p0: String?) = null
+ override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?) = null
+ override fun updateCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
+ override fun getAuthToken(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
+ override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?) = null
+
+ }
+
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooTasksSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooTasksSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d4f52eb9d0870233059f8a31b5adf079c3caa548
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/YahooTasksSyncAdapterService.kt
@@ -0,0 +1,174 @@
+/*
+ * 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 foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.app.PendingIntent
+import android.content.*
+import android.content.pm.PackageManager
+import android.graphics.drawable.BitmapDrawable
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.AsyncTask
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import foundation.e.accountmanager.R
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.model.AppDatabase
+import foundation.e.accountmanager.model.Collection
+import foundation.e.accountmanager.model.Credentials
+import foundation.e.accountmanager.model.Service
+import foundation.e.accountmanager.resource.LocalTaskList
+import foundation.e.accountmanager.settings.AccountSettings
+import foundation.e.accountmanager.ui.NotificationUtils
+import foundation.e.ical4android.AndroidTaskList
+import foundation.e.ical4android.TaskProvider
+import net.openid.appauth.AuthorizationService
+import okhttp3.HttpUrl
+import org.dmfs.tasks.contract.TaskContract
+import java.util.logging.Level
+
+/**
+ * Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}).
+ */
+class YahooTasksSyncAdapterService: SyncAdapterService() {
+
+ override fun syncAdapter() = TasksSyncAdapter(this)
+
+
+ class TasksSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ try {
+ val taskProvider = TaskProvider.fromProviderClient(context, provider)
+
+ // make sure account can be seen by OpenTasks
+ if (Build.VERSION.SDK_INT >= 26)
+ AccountManager.get(context).setAccountVisibility(account, taskProvider.name.packageName, AccountManager.VISIBILITY_VISIBLE)
+
+ val accountSettings = AccountSettings(context, account)
+ /* don't run sync if
+ - sync conditions (e.g. "sync only in WiFi") are not met AND
+ - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
+ */
+ if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
+ return
+
+ updateLocalTaskLists(taskProvider, account, accountSettings)
+
+ val priorityTaskLists = priorityCollections(extras)
+ val taskLists = AndroidTaskList
+ .find(account, taskProvider, LocalTaskList.Factory, "${TaskContract.TaskLists.SYNC_ENABLED}!=0", null)
+ .sortedByDescending { priorityTaskLists.contains(it.id) }
+ for (taskList in taskLists) {
+ Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]")
+ TasksSyncManager(context, account, accountSettings, extras, authority, syncResult, taskList).use {
+ val authState = accountSettings.credentials().authState
+ if (authState != null)
+ {
+ if (authState.needsTokenRefresh)
+ {
+ val tokenRequest = authState.createTokenRefreshRequest()
+
+ AuthorizationService(context).performTokenRequest(tokenRequest,
+ AuthorizationService.TokenResponseCallback { tokenResponse, ex ->
+ authState.update(tokenResponse, ex)
+ accountSettings.credentials(Credentials(account.name, null, authState, null))
+ it.accountSettings.credentials(Credentials(it.account.name, null, authState, null))
+ object : AsyncTask() {
+ override fun doInBackground(vararg params: Void): Void? {
+ it.performSync()
+ return null
+ }
+ }.execute()
+ })
+ }
+ else {
+ it.performSync()
+ }
+ }
+ else {
+ it.performSync()
+ }
+ }
+ }
+ } catch (e: TaskProvider.ProviderTooOldException) {
+ val nm = NotificationManagerCompat.from(context)
+ val message = context.getString(R.string.sync_error_opentasks_required_version, e.provider.minVersionName, e.installedVersionName)
+ val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_ERRORS)
+ .setSmallIcon(R.drawable.ic_sync_problem_notify)
+ .setContentTitle(context.getString(R.string.sync_error_opentasks_too_old))
+ .setContentText(message)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(message))
+ .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(NotificationUtils.NOTIFY_OPENTASKS, 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")
+ }
+
+ private fun updateLocalTaskLists(provider: TaskProvider, account: Account, settings: AccountSettings) {
+ val db = AppDatabase.getInstance(context)
+ val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
+
+ val remoteTaskLists = mutableMapOf()
+ if (service != null)
+ for (collection in db.collectionDao().getSyncTaskLists(service.id)) {
+ remoteTaskLists[collection.url] = collection
+ }
+
+ // delete/update local task lists
+ val updateColors = settings.getManageCalendarColors()
+
+ for (list in AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null))
+ list.syncId?.let {
+ val url = HttpUrl.parse(it)!!
+ val info = remoteTaskLists[url]
+ if (info == null) {
+ Logger.log.fine("Deleting obsolete local task list $url")
+ list.delete()
+ } else {
+ // remote CollectionInfo found for this local collection, update data
+ Logger.log.log(Level.FINE, "Updating local task list $url", info)
+ list.update(info, updateColors)
+ // we already have a local task list for this remote collection, don't take into consideration anymore
+ remoteTaskLists -= url
+ }
+ }
+
+ // create new local task lists
+ for ((_,info) in remoteTaskLists) {
+ Logger.log.log(Level.INFO, "Adding local task list", info)
+ LocalTaskList.create(account, provider, info)
+ }
+ }
+
+ }
+
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/ui/setup/LoginActivity.kt b/app/src/main/java/foundation/e/accountmanager/ui/setup/LoginActivity.kt
index b25dbb90fe534dcc5d6c69b24b43b173f98c736b..d3d3e83e6bae02389037117dc115a2c0c529ec6f 100644
--- a/app/src/main/java/foundation/e/accountmanager/ui/setup/LoginActivity.kt
+++ b/app/src/main/java/foundation/e/accountmanager/ui/setup/LoginActivity.kt
@@ -45,6 +45,7 @@ class LoginActivity: AppCompatActivity() {
const val SETUP_ACCOUNT_PROVIDER_TYPE = "setup_account_provider_type"
const val ACCOUNT_PROVIDER_EELO = "eelo"
const val ACCOUNT_PROVIDER_GOOGLE = "google"
+ const val ACCOUNT_PROVIDER_YAHOO = "yahoo"
const val ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE = "google_auth_complete"
}
diff --git a/app/src/main/res/drawable/ic_account_provider_yahoo.png b/app/src/main/res/drawable/ic_account_provider_yahoo.png
new file mode 100644
index 0000000000000000000000000000000000000000..944c01f17e14dd2c5d638aff47bdef63115fb12c
Binary files /dev/null and b/app/src/main/res/drawable/ic_account_provider_yahoo.png differ
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 128ef32c0e21d12146305398598a8a58793861ab..941a8cf8f1720ee8d63b4706c5a1fd4984eb22c3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -14,12 +14,16 @@
e.foundation.webdav
Google
e.foundation.webdav.google
+ Yahoo
+ e.foundation.webdav.yahoo
/e/
e.foundation.webdav.eelo
foundation.e.accountmanager.address_book
WebDav Address book
foundation.e.accountmanager.eelo.address_book
/e/ Address book
+ foundation.e.accountmanager.yahoo.address_book
+ Yahoo Address book
foundation.e.accountmanager.google.address_book
Google Address book
foundation.e.accountmanager.addressbooks
diff --git a/app/src/main/res/xml/account_authenticator_yahoo_address_book.xml b/app/src/main/res/xml/account_authenticator_yahoo_address_book.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a76937693d07344fd695664c038a54a88c7b3b78
--- /dev/null
+++ b/app/src/main/res/xml/account_authenticator_yahoo_address_book.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/app/src/main/res/xml/yahoo_account_authenticator.xml b/app/src/main/res/xml/yahoo_account_authenticator.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b3c8bfa39f446632ca3b8e5775432659dc9d23e4
--- /dev/null
+++ b/app/src/main/res/xml/yahoo_account_authenticator.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/app/src/main/res/xml/yahoo_sync_address_books.xml b/app/src/main/res/xml/yahoo_sync_address_books.xml
new file mode 100644
index 0000000000000000000000000000000000000000..83981e7a5fa35bc1aefefabf108d162ebdf13506
--- /dev/null
+++ b/app/src/main/res/xml/yahoo_sync_address_books.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/app/src/main/res/xml/yahoo_sync_calendars.xml b/app/src/main/res/xml/yahoo_sync_calendars.xml
new file mode 100644
index 0000000000000000000000000000000000000000..39a900306e1e8099c5e89edec59bfabaa08f4aaf
--- /dev/null
+++ b/app/src/main/res/xml/yahoo_sync_calendars.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/app/src/main/res/xml/yahoo_sync_contacts.xml b/app/src/main/res/xml/yahoo_sync_contacts.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cd66cebcf2d611455e8ea60b2cce31264fe4e1ff
--- /dev/null
+++ b/app/src/main/res/xml/yahoo_sync_contacts.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/app/src/main/res/xml/yahoo_sync_email.xml b/app/src/main/res/xml/yahoo_sync_email.xml
new file mode 100644
index 0000000000000000000000000000000000000000..15f1325b5145bbab478674213edd96cd24e4bf6d
--- /dev/null
+++ b/app/src/main/res/xml/yahoo_sync_email.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/app/src/main/res/xml/yahoo_sync_tasks.xml b/app/src/main/res/xml/yahoo_sync_tasks.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d71ef3a333e3d83533325d4ea3221fd9ca2c062a
--- /dev/null
+++ b/app/src/main/res/xml/yahoo_sync_tasks.xml
@@ -0,0 +1,15 @@
+
+
+