From 31190b5a8730d4e74c26b58849a53b117ed7b07f Mon Sep 17 00:00:00 2001 From: Seweryn Fornalik Date: Wed, 30 Jun 2021 16:46:24 +0200 Subject: [PATCH] Add Yahoo account support --- app/src/main/AndroidManifest.xml | 90 +++++++-- .../YahooAccountAuthenticatorService.kt | 159 ++++++++++++++++ .../YahooAddressBooksSyncAdapterService.kt | 129 +++++++++++++ .../YahooCalendarsSyncAdapterService.kt | 137 ++++++++++++++ .../YahooContactsSyncAdapterService.kt | 79 ++++++++ .../YahooEmailSyncAdapterService.kt | 29 +++ .../YahooNullAuthenticatorService.kt | 54 ++++++ .../YahooTasksSyncAdapterService.kt | 174 ++++++++++++++++++ .../accountmanager/ui/setup/LoginActivity.kt | 1 + .../drawable/ic_account_provider_yahoo.png | Bin 0 -> 4695 bytes app/src/main/res/values/strings.xml | 4 + ...count_authenticator_yahoo_address_book.xml | 15 ++ .../res/xml/yahoo_account_authenticator.xml | 15 ++ .../main/res/xml/yahoo_sync_address_books.xml | 14 ++ app/src/main/res/xml/yahoo_sync_calendars.xml | 17 ++ app/src/main/res/xml/yahoo_sync_contacts.xml | 15 ++ app/src/main/res/xml/yahoo_sync_email.xml | 14 ++ app/src/main/res/xml/yahoo_sync_tasks.xml | 15 ++ 18 files changed, 945 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/foundation/e/accountmanager/syncadapter/YahooAccountAuthenticatorService.kt create mode 100644 app/src/main/java/foundation/e/accountmanager/syncadapter/YahooAddressBooksSyncAdapterService.kt create mode 100644 app/src/main/java/foundation/e/accountmanager/syncadapter/YahooCalendarsSyncAdapterService.kt create mode 100644 app/src/main/java/foundation/e/accountmanager/syncadapter/YahooContactsSyncAdapterService.kt create mode 100644 app/src/main/java/foundation/e/accountmanager/syncadapter/YahooEmailSyncAdapterService.kt create mode 100644 app/src/main/java/foundation/e/accountmanager/syncadapter/YahooNullAuthenticatorService.kt create mode 100644 app/src/main/java/foundation/e/accountmanager/syncadapter/YahooTasksSyncAdapterService.kt create mode 100644 app/src/main/res/drawable/ic_account_provider_yahoo.png create mode 100644 app/src/main/res/xml/account_authenticator_yahoo_address_book.xml create mode 100644 app/src/main/res/xml/yahoo_account_authenticator.xml create mode 100644 app/src/main/res/xml/yahoo_sync_address_books.xml create mode 100644 app/src/main/res/xml/yahoo_sync_calendars.xml create mode 100644 app/src/main/res/xml/yahoo_sync_contacts.xml create mode 100644 app/src/main/res/xml/yahoo_sync_email.xml create mode 100644 app/src/main/res/xml/yahoo_sync_tasks.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 66670365a..7acc6ca44 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 000000000..763f49ff3 --- /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 000000000..16b148abd --- /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 000000000..540e97739 --- /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 000000000..7e677399f --- /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 000000000..a6faf1264 --- /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 000000000..e8f1ea281 --- /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 000000000..d4f52eb9d --- /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 b25dbb90f..d3d3e83e6 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 GIT binary patch literal 4695 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 Z{|`gFfcXMD%Wwby002ovPDHLkV1hGl6=47X literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 128ef32c0..941a8cf8f 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 000000000..a76937693 --- /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 000000000..b3c8bfa39 --- /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 000000000..83981e7a5 --- /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 000000000..39a900306 --- /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 000000000..cd66cebcf --- /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 000000000..15f1325b5 --- /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 000000000..d71ef3a33 --- /dev/null +++ b/app/src/main/res/xml/yahoo_sync_tasks.xml @@ -0,0 +1,15 @@ + + + -- GitLab