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