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 @@ + + +