Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 31190b5a authored by Seweryn Fornalik's avatar Seweryn Fornalik
Browse files

Add Yahoo account support

parent b4134700
Loading
Loading
Loading
Loading
Loading
+74 −16
Original line number Diff line number Diff line
@@ -336,9 +336,9 @@
                android:resource="@xml/contacts" />
        </service>
	
	<!-- account type "Google" -->
	<!-- account type "Yahoo" -->
        <service
            android:name=".syncadapter.GoogleAccountAuthenticatorService"
            android:name=".syncadapter.YahooAccountAuthenticatorService"
            android:exported="false">
            <intent-filter>
                <action android:name="android.accounts.AccountAuthenticator" />
@@ -346,11 +346,11 @@

            <meta-data
                android:name="android.accounts.AccountAuthenticator"
                android:resource="@xml/google_account_authenticator" />
                android:resource="@xml/yahoo_account_authenticator" />
        </service>

	<service
            android:name=".syncadapter.GoogleCalendarsSyncAdapterService"
            android:name=".syncadapter.YahooCalendarsSyncAdapterService"
            android:exported="true"
            android:process=":sync"
            tools:ignore="ExportedService">
@@ -360,11 +360,11 @@

            <meta-data
                android:name="android.content.SyncAdapter"
                android:resource="@xml/google_sync_calendars" />
                android:resource="@xml/yahoo_sync_calendars" />
	</service>

        <service
            android:name=".syncadapter.GoogleTasksSyncAdapterService"
            android:name=".syncadapter.YahooTasksSyncAdapterService"
            android:exported="true"
            android:process=":sync"
            tools:ignore="ExportedService">
@@ -374,12 +374,70 @@

            <meta-data
                android:name="android.content.SyncAdapter"
                android:resource="@xml/google_sync_tasks" />
                android:resource="@xml/yahoo_sync_tasks" />
	</service>	
	
	<!-- account type "Yahoo Address book" -->
        <service
            android:name=".syncadapter.YahooNullAuthenticatorService"
            android:exported="false">
            <intent-filter>
                <action android:name="android.accounts.AccountAuthenticator" />
            </intent-filter>

            <meta-data
                android:name="android.accounts.AccountAuthenticator"
                android:resource="@xml/account_authenticator_yahoo_address_book" />
        </service>

        <service
            android:name=".syncadapter.YahooAddressBooksSyncAdapterService"
            android:exported="true"
            android:process=":sync"
            tools:ignore="ExportedService">
            <intent-filter>
                <action android:name="android.content.SyncAdapter" />
            </intent-filter>

            <meta-data
                android:name="android.content.SyncAdapter"
                android:resource="@xml/yahoo_sync_address_books" />
	</service>

        <service
            android:name=".syncadapter.YahooContactsSyncAdapterService"
            android:exported="true"
            android:process=":sync"
            tools:ignore="ExportedService">
            <intent-filter>
                <action android:name="android.content.SyncAdapter" />
            </intent-filter>

            <meta-data
                android:name="android.content.SyncAdapter"
                android:resource="@xml/yahoo_sync_contacts" />
            <meta-data
                android:name="android.provider.CONTACTS_STRUCTURE"
                android:resource="@xml/contacts" />
        </service>
	
	<service
            android:name=".syncadapter.YahooEmailSyncAdapterService"
            android:exported="true"
            android:process=":sync"
            tools:ignore="ExportedService">
            <intent-filter>
                <action android:name="android.content.SyncAdapter" />
            </intent-filter>

            <meta-data
                android:name="android.content.SyncAdapter"
                android:resource="@xml/yahoo_sync_email" />
        </service>

        <!-- account type "Google Address book" -->
        <service
            android:name=".syncadapter.GoogleNullAuthenticatorService"
            android:name=".syncadapter.YahooNullAuthenticatorService"
            android:exported="false">
            <intent-filter>
                <action android:name="android.accounts.AccountAuthenticator" />
@@ -387,11 +445,11 @@

            <meta-data
                android:name="android.accounts.AccountAuthenticator"
                android:resource="@xml/account_authenticator_google_address_book" />
                android:resource="@xml/account_authenticator_yahoo_address_book" />
        </service>

        <service
            android:name=".syncadapter.GoogleAddressBooksSyncAdapterService"
            android:name=".syncadapter.YahooAddressBooksSyncAdapterService"
            android:exported="true"
            android:process=":sync"
            tools:ignore="ExportedService">
@@ -401,11 +459,11 @@

            <meta-data
                android:name="android.content.SyncAdapter"
                android:resource="@xml/google_sync_address_books" />
                android:resource="@xml/yahoo_sync_address_books" />
        </service>

        <service
            android:name=".syncadapter.GoogleContactsSyncAdapterService"
            android:name=".syncadapter.YahooContactsSyncAdapterService"
            android:exported="true"
            android:process=":sync"
            tools:ignore="ExportedService">
@@ -415,14 +473,14 @@

            <meta-data
                android:name="android.content.SyncAdapter"
                android:resource="@xml/google_sync_contacts" />
                android:resource="@xml/yahoo_sync_contacts" />
            <meta-data
                android:name="android.provider.CONTACTS_STRUCTURE"
                android:resource="@xml/contacts" />
        </service>

        <service
            android:name=".syncadapter.GoogleEmailSyncAdapterService"
            android:name=".syncadapter.YahooEmailSyncAdapterService"
            android:exported="true"
            android:process=":sync"
            tools:ignore="ExportedService">
@@ -432,7 +490,7 @@

            <meta-data
                android:name="android.content.SyncAdapter"
                android:resource="@xml/google_sync_email" />
                android:resource="@xml/yahoo_sync_email" />
        </service>

	<!-- Callback from authentication screen -->
+159 −0
Original line number Diff line number Diff line
/*
 * 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<String>()
	    val accounts = ArrayList<Account>()
            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<Account>()
            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<out Account>?) {
	thread {
            cleanupAccounts(this)
        }
    }


    private class AccountAuthenticator(
            val context: Context
    ) : AbstractAccountAuthenticator(context) {

        override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array<String>?, 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<out String>?) = null

    }
}
+129 −0
Original line number Diff line number Diff line
/*
 * 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<HttpUrl, Collection>()
            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
        }

    }

}
+137 −0

File added.

Preview size limit exceeded, changes collapsed.

+79 −0
Original line number Diff line number Diff line
/*
 * 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")
        }

    }

}
Loading