diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1fb492915e226078737541c4441ef46a9041215f..5d3f9f1d46c8e8baef0ecd264daf67262ce9f609 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -405,7 +405,7 @@ @@ -490,7 +490,7 @@ @@ -546,6 +546,105 @@ android:resource="@xml/google_sync_email" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { val mailboxes = LinkedList() try { - DavResource(httpClient.okHttpClient, principal, null, log).propfind(0, CalendarUserAddressSet.NAME) { response, _ -> + DavResource(httpClient.okHttpClient, principal, loginModel.credentials?.authState?.accessToken, log).propfind(0, CalendarUserAddressSet.NAME) { response, _ -> response[CalendarUserAddressSet::class.java]?.let { addressSet -> for (href in addressSet.hrefs) try { diff --git a/app/src/main/java/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt b/app/src/main/java/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt index 6e186afbb3b7d6164418eec56bd3d66e62d2da41..613ec99c8830826df520397de8cca5eac61c50d9 100644 --- a/app/src/main/java/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt +++ b/app/src/main/java/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt @@ -321,7 +321,12 @@ class RefreshCollectionsWorker @AssistedInject constructor( // this collection doesn't belong to a homeset anymore, otherwise it would have been confirmed info.homeSetId = null - DavResource(httpClient, url).propfind(0, *DAV_COLLECTION_PROPERTIES) { response, _ -> + var accessToken : String? = null + service.authState?.let { + accessToken = AuthState.jsonDeserialize(it).accessToken + } + + DavResource(httpClient, url, accessToken).propfind(0, *DAV_COLLECTION_PROPERTIES) { response, _ -> if (!response.isSuccess()) return@propfind diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountUtils.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountUtils.kt index 840e24b216812ef266de69522ed38a71762e1832..cbe2ba8ebdf6e33c8cbb04fb4fbf510a8c15fb1d 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountUtils.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountUtils.kt @@ -73,7 +73,8 @@ object AccountUtils { listOf( context.getString(R.string.account_type), context.getString(R.string.eelo_account_type), - context.getString(R.string.google_account_type) + context.getString(R.string.google_account_type), + context.getString(R.string.yahoo_account_type) ) fun getMainAccounts(context: Context): List { @@ -92,7 +93,8 @@ object AccountUtils { listOf( context.getString(R.string.account_type_address_book), context.getString(R.string.account_type_eelo_address_book), - context.getString(R.string.account_type_google_address_book) + context.getString(R.string.account_type_google_address_book), + context.getString(R.string.account_type_yahoo_address_book) ) fun getAddressBookAccounts(context: Context): List { @@ -107,12 +109,21 @@ object AccountUtils { return accounts } - fun getAddressBookType(context: Context, mainType: String): String? { - return when(mainType) { - context.getString(R.string.account_type) -> context.getString(R.string.account_type_address_book) - context.getString(R.string.eelo_account_type) -> context.getString(R.string.account_type_eelo_address_book) - context.getString(R.string.google_account_type) -> context.getString(R.string.account_type_google_address_book) - else -> null - } + fun getOpenIdMainAccountTypes(context: Context) = + listOf( + context.getString(R.string.google_account_type), + context.getString(R.string.yahoo_account_type) + ) + + fun getOpenIdMainAccounts(context: Context): List { + val accountManager = AccountManager.get(context) + val accounts = mutableListOf() + + getOpenIdMainAccountTypes(context) + .forEach { + accounts.addAll(accountManager.getAccountsByType(it)) + } + + return accounts } -} \ No newline at end of file +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt index ccc96757795ce5303c5965c828fdb1ea05636b49..81ad168b10a783d656bebf9c8a9e6e90602042ae 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AddressBooksSyncAdapterService.kt @@ -31,7 +31,7 @@ import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import java.util.logging.Level -class AddressBooksSyncAdapterService : SyncAdapterService() { +open class AddressBooksSyncAdapterService : SyncAdapterService() { override fun syncAdapter() = AddressBooksSyncAdapter(this, appDatabase) @@ -81,8 +81,12 @@ class AddressBooksSyncAdapterService : SyncAdapterService() { val remoteAddressBooks = mutableMapOf() if (service != null) - for (collection in db.collectionDao().getByServiceAndSync(service.id)) + for (collection in db.collectionDao().getByServiceAndSync(service.id)) { + if (collection.url.toString().contains(AccountSettings.CONTACTS_APP_INTERACTION)) { + db.collectionDao().delete(collection) + } remoteAddressBooks[collection.url] = collection + } if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED) { if (remoteAddressBooks.isEmpty()) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt index ae4bc54cce04e1e15356f1efbfca10033dcdf901..7bc61773dfe33ff6ec727b94558f6c0a8a7a79e1 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt @@ -54,7 +54,8 @@ open class CalendarsSyncAdapterService : SyncAdapterService() { - 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)) { + !checkSyncConditions(accountSettings) + ) { return } @@ -86,52 +87,7 @@ open class CalendarsSyncAdapterService : SyncAdapterService() { authority, syncResult, calendar - ).let { - val authState = accountSettings.credentials().authState - if (authState != null) { - if (authState.needsTokenRefresh) { - val tokenRequest = authState.createTokenRefreshRequest() - val clientSecretString = accountSettings.credentials().clientSecret - val clientSecret = - OpenIdUtils.getClientAuthentication(clientSecretString) - - AuthorizationService(context).performTokenRequest( - tokenRequest, - clientSecret - ) { tokenResponse, ex -> - authState.update(tokenResponse, ex) - accountSettings.credentials( - Credentials( - account.name, - null, - authState, - null, - clientSecret = clientSecretString - ) - ) - it.accountSettings.credentials( - Credentials( - it.account.name, - null, - authState, - null, - clientSecret = clientSecretString - ) - ) - object : AsyncTask() { - override fun doInBackground(vararg params: Void): Void? { - it.performSyncWithRetry() - return null - } - }.execute() - } - } else { - it.performSyncWithRetry() - } - } else { - it.performSyncWithRetry() - } - } + ).performSync() } } catch (e: Exception) { Logger.log.log(Level.SEVERE, "Couldn't sync calendars", e) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt index bb89b01a83b9110d87cf413c44e16e86b9c742c6..407af4a97d2d3621274a93b089cd7dc8dcdc8f26 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt @@ -22,7 +22,7 @@ import at.bitfire.davdroid.settings.AccountSettings import net.openid.appauth.AuthorizationService import java.util.logging.Level -class ContactsSyncAdapterService: SyncAdapterService() { +open class ContactsSyncAdapterService: SyncAdapterService() { companion object { const val PREVIOUS_GROUP_METHOD = "previous_group_method" @@ -67,48 +67,7 @@ class ContactsSyncAdapterService: SyncAdapterService() { Logger.log.info("Synchronizing address book: ${addressBook.url}") Logger.log.info("Taking settings from: ${addressBook.mainAccount}") - ContactsSyncManager(context, account, accountSettings, httpClient.value, extras, authority, syncResult, provider, addressBook).let { - val authState = accountSettings.credentials().authState - if (authState != null) { - if (authState.needsTokenRefresh) { - val tokenRequest = authState.createTokenRefreshRequest() - val clientSecretString = accountSettings.credentials().clientSecret - val clientSecret = OpenIdUtils.getClientAuthentication(clientSecretString) - - AuthorizationService(context).performTokenRequest(tokenRequest, clientSecret) { tokenResponse, ex -> - authState.update(tokenResponse, ex) - accountSettings.credentials( - Credentials( - account.name, - null, - authState, - null, - clientSecret = clientSecretString - ) - ) - it.accountSettings.credentials( - Credentials( - it.account.name, - null, - authState, - null, - clientSecret = clientSecretString - ) - ) - object : AsyncTask() { - override fun doInBackground(vararg params: Void): Void? { - it.performSync() - return null - } - }.execute() - } - } else { - it.performSync() - } - } else { - it.performSync() - } - } + ContactsSyncManager(context, account, accountSettings, httpClient.value, extras, authority, syncResult, provider, addressBook).performSync() } catch(e: Exception) { Logger.log.log(Level.SEVERE, "Couldn't sync contacts", e) } diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/DefaultAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/DefaultAccountAuthenticatorService.kt new file mode 100644 index 0000000000000000000000000000000000000000..709cc45746c5e546bd9d3c1cb853b709084f0263 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/DefaultAccountAuthenticatorService.kt @@ -0,0 +1,225 @@ +/* + * Copyright MURENA SAS 2023 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.syncadapter + +import android.accounts.AbstractAccountAuthenticator +import android.accounts.Account +import android.accounts.AccountAuthenticatorResponse +import android.accounts.AccountManager +import android.accounts.OnAccountsUpdateListener +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.annotation.AnyThread +import at.bitfire.davdroid.OpenIdUtils +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.ui.setup.LoginActivity +import dagger.hilt.android.EntryPointAccessors +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import net.openid.appauth.AuthState +import net.openid.appauth.AuthorizationService +import java.util.logging.Level + +abstract class DefaultAccountAuthenticatorService : Service(), OnAccountsUpdateListener { + + companion object { + fun cleanupAccounts(context: Context, db: AppDatabase) { + Logger.log.info("Cleaning up orphaned accounts") + + val accountNames = HashSet() + val accounts = AccountUtils.getMainAccounts(context) + + for (account in accounts.toTypedArray()) { + accountNames += account.name + } + + // delete orphaned address book accounts + val addressBookAccounts = AccountUtils.getAddressBookAccounts(context) + 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 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() { + accountManager.removeOnAccountsUpdatedListener(this) + super.onDestroy() + } + + override fun onBind(intent: Intent?) = + accountAuthenticator.iBinder.takeIf { intent?.action == AccountManager.ACTION_AUTHENTICATOR_INTENT } + + @AnyThread + override fun onAccountsUpdated(accounts: Array?) { + CoroutineScope(Dispatchers.IO).launch { + val db = EntryPointAccessors.fromApplication( + applicationContext, + AccountsUpdatedListener.AccountsUpdatedListenerEntryPoint::class.java + ).appDatabase() + + cleanupAccounts(applicationContext, db) + } + } + + 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.ACCOUNT_TYPE, + accountType + ) + + options?.let { + intent.putExtra( + LoginActivity.OPEN_APP_PACKAGE_AFTER_AUTH, + it.getString(LoginActivity.OPEN_APP_PACKAGE_AFTER_AUTH) + ) + intent.putExtra( + LoginActivity.OPEN_APP_ACTIVITY_AFTER_AUTH, + it.getString(LoginActivity.OPEN_APP_ACTIVITY_AFTER_AUTH) + ) + } + + 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( + rssponse: AccountAuthenticatorResponse?, + account: Account?, + options: Bundle? + ) = null + + override fun updateCredentials( + response: AccountAuthenticatorResponse?, + account: Account?, + authTokenType: String?, + options: Bundle? + ) = null + + override fun getAuthToken( + response: AccountAuthenticatorResponse?, + account: Account?, + authTokenType: String?, + options: Bundle? + ): Bundle? { + val accountManager = AccountManager.get(context) + val authStateString = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) ?: return null + + val authState = AuthState.jsonDeserialize(authStateString) + + if (authState == null) { + val result = Bundle() + result.putInt( + AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, + AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION + ) + return result + } + + if (!authState.needsTokenRefresh) { + 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 tokenRequest = authState.createTokenRefreshRequest() + val clientSecretString = accountManager.getUserData(account, AccountSettings.KEY_CLIENT_SECRET) + val clientSecret = OpenIdUtils.getClientAuthentication(clientSecretString) + + AuthorizationService(context).performTokenRequest( + tokenRequest, + clientSecret + ) { 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) + } + + val result = Bundle() + result.putInt( + AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, + AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION + ) + return result + } + + override fun hasFeatures( + response: AccountAuthenticatorResponse?, + account: Account?, + features: Array? + ) = null + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloNullAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/DefaultAddressBookAuthenticatorService.kt similarity index 53% rename from app/src/main/java/at/bitfire/davdroid/syncadapter/EeloNullAuthenticatorService.kt rename to app/src/main/java/at/bitfire/davdroid/syncadapter/DefaultAddressBookAuthenticatorService.kt index 58367a45a433170ebd315a53beb14c7c5042e0e7..663f8976ab49c79683f186a5972e8037267eebb9 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloNullAuthenticatorService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/DefaultAddressBookAuthenticatorService.kt @@ -1,5 +1,5 @@ /* - * Copyright ECORP SAS 2022 + * Copyright MURENA SAS 2023 * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -26,7 +26,7 @@ import android.content.Intent import android.os.Bundle import at.bitfire.davdroid.ui.AccountsActivity -class EeloNullAuthenticatorService: Service() { +abstract class DefaultAddressBookAuthenticatorService : Service() { private lateinit var accountAuthenticator: AccountAuthenticator @@ -35,14 +35,20 @@ class EeloNullAuthenticatorService: Service() { } override fun onBind(intent: Intent?) = - accountAuthenticator.iBinder.takeIf { intent?.action == AccountManager.ACTION_AUTHENTICATOR_INTENT } + accountAuthenticator.iBinder.takeIf { intent?.action == AccountManager.ACTION_AUTHENTICATOR_INTENT } private class AccountAuthenticator( - val context: Context - ): AbstractAccountAuthenticator(context) { + val context: Context + ) : AbstractAccountAuthenticator(context) { - override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?): Bundle { + 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) @@ -50,12 +56,34 @@ class EeloNullAuthenticatorService: Service() { 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 + override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = + null + + override fun getAuthTokenLabel(authTokenType: String?) = null + override fun confirmCredentials( + response: AccountAuthenticatorResponse?, + account: Account?, + options: Bundle? + ) = null + + override fun updateCredentials( + response: AccountAuthenticatorResponse?, + account: Account?, + authTokenType: String?, + options: Bundle? + ) = null + + override fun getAuthToken( + response: AccountAuthenticatorResponse?, + account: Account?, + authTokenType: String?, + options: Bundle? + ) = null + + override fun hasFeatures( + response: AccountAuthenticatorResponse?, + account: Account?, + features: Array? + ) = null } } - diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt index 93ef72e1f7529ef7a0d045b5ebf685e91e8c6342..c78df33b3d88cc444076b65eea4241eb7a1761b9 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt @@ -43,125 +43,4 @@ import java.util.logging.Level * and contains the corresponding cleanup code. */ -class EeloAccountAuthenticatorService : Service(), OnAccountsUpdateListener { - - companion object { - - fun cleanupAccounts(context: Context, db: AppDatabase) { - Logger.log.info("Cleaning up orphaned accounts") - - val accountNames = HashSet() - - val accounts = AccountUtils.getMainAccounts(context) - - for (account in accounts.toTypedArray()) { - accountNames += account.name - } - - // delete orphaned address book accounts - val addressBookAccounts = AccountUtils.getAddressBookAccounts(context) - 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 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 == AccountManager.ACTION_AUTHENTICATOR_INTENT } - - override fun onAccountsUpdated(accounts: Array?) { - /* onAccountsUpdated may be called from the main thread, but cleanupAccounts - requires disk (database) access. So we launch it in a separate thread. */ - CoroutineScope(Dispatchers.Default).launch { - val db = EntryPointAccessors.fromApplication( - applicationContext, - AccountsUpdatedListener.AccountsUpdatedListenerEntryPoint::class.java - ).appDatabase() - - cleanupAccounts(applicationContext, db) - } - } - - 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.ACCOUNT_TYPE, - context.getString(R.string.eelo_account_type) - ) - 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? - ) = null - - override fun hasFeatures( - p0: AccountAuthenticatorResponse?, - p1: Account?, - p2: Array? - ) = null - } -} +class EeloAccountAuthenticatorService : DefaultAccountAuthenticatorService() diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAddressBookAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAddressBookAuthenticatorService.kt new file mode 100644 index 0000000000000000000000000000000000000000..467063347a9d29eaf3c51d5cab1eabe72aaea29c --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAddressBookAuthenticatorService.kt @@ -0,0 +1,20 @@ +/* + * Copyright ECORP SAS 2022 + * Copyright MURENA SAS 2023 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.syncadapter + +class EeloAddressBookAuthenticatorService : DefaultAddressBookAuthenticatorService() \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAddressBooksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAddressBooksSyncAdapterService.kt index aa07a004eb834149436081aa30e8d977a3f0da53..619cc154c956b363dbfbc87ae98930f58ddcdb0b 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAddressBooksSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAddressBooksSyncAdapterService.kt @@ -16,153 +16,4 @@ package at.bitfire.davdroid.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 at.bitfire.davdroid.HttpClient -import at.bitfire.davdroid.PermissionUtils -import at.bitfire.davdroid.closeCompat -import at.bitfire.davdroid.db.AppDatabase -import at.bitfire.davdroid.db.Collection -import at.bitfire.davdroid.db.Service -import at.bitfire.davdroid.log.Logger -import at.bitfire.davdroid.resource.LocalAddressBook -import at.bitfire.davdroid.settings.AccountSettings -import at.bitfire.davdroid.ui.account.AccountActivity -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import java.util.logging.Level - -class EeloAddressBooksSyncAdapterService : SyncAdapterService() { - - override fun syncAdapter() = AddressBooksSyncAdapter(this, appDatabase) - - class AddressBooksSyncAdapter( - context: Context, - db: AppDatabase - ) : SyncAdapter(context, db) { - - override fun sync( - account: Account, - extras: Bundle, - authority: String, - httpClient: Lazy, - 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 service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CARDDAV) - - val remoteAddressBooks = mutableMapOf() - if (service != null) - for (collection in db.collectionDao().getByServiceAndSync(service.id)) { - if(collection.url.toString().contains(AccountSettings.CONTACTS_APP_INTERACTION)) { - db.collectionDao().delete(collection) - } - 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) - - PermissionUtils.notifyPermissions(context, 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 = addressBook.url.toHttpUrlOrNull()!! - 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, false) - } 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, db, contactsProvider, account, info, false) - } - } finally { - contactsProvider?.closeCompat() - } - - return true - } - - } - -} - +class EeloAddressBooksSyncAdapterService : AddressBooksSyncAdapterService() diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloContactsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloContactsSyncAdapterService.kt index 52ca8b17f92b79da8b119ff54157c028ef1f7d53..0034ab3d03da088f4b06c2f6e461fce9587efbea 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloContactsSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloContactsSyncAdapterService.kt @@ -16,80 +16,4 @@ package at.bitfire.davdroid.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 at.bitfire.davdroid.HttpClient -import at.bitfire.davdroid.db.AppDatabase -import at.bitfire.davdroid.log.Logger -import at.bitfire.davdroid.resource.LocalAddressBook -import at.bitfire.davdroid.settings.AccountSettings -import java.util.logging.Level - -class EeloContactsSyncAdapterService: SyncAdapterService() { - - companion object { - const val PREVIOUS_GROUP_METHOD = "previous_group_method" - } - - override fun syncAdapter() = ContactsSyncAdapter(this, appDatabase) - - - class ContactsSyncAdapter( - context: Context, - db: AppDatabase - ): SyncAdapter(context, db) { - - override fun sync( - account: Account, - extras: Bundle, - authority: String, - httpClient: Lazy, - 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, httpClient.value, extras, authority, syncResult, provider, addressBook).let { - it.performSyncWithRetry() - } - } catch(e: Exception) { - Logger.log.log(Level.SEVERE, "Couldn't sync contacts", e) - } - Logger.log.info("Contacts sync complete") - } - } -} - +class EeloContactsSyncAdapterService: ContactsSyncAdapterService() diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt index 37fdb1429683ac28569d3ac9fc804527322d77af..a8a680c552fafbde00c4a093e9ff24ac763cee41 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt @@ -16,30 +16,6 @@ package at.bitfire.davdroid.syncadapter -import android.accounts.AbstractAccountAuthenticator -import android.accounts.Account -import android.accounts.AccountAuthenticatorResponse -import android.accounts.AccountManager -import android.accounts.OnAccountsUpdateListener -import android.app.Service -import android.content.Context -import android.content.Intent -import android.os.Bundle -import at.bitfire.davdroid.OpenIdUtils -import at.bitfire.davdroid.R -import at.bitfire.davdroid.db.AppDatabase -import at.bitfire.davdroid.log.Logger -import at.bitfire.davdroid.resource.LocalAddressBook -import at.bitfire.davdroid.settings.AccountSettings -import at.bitfire.davdroid.ui.setup.LoginActivity -import dagger.hilt.android.EntryPointAccessors -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import net.openid.appauth.AuthState -import net.openid.appauth.AuthorizationService -import java.util.logging.Level - /** * Account authenticator for the Google account type. * @@ -47,168 +23,4 @@ import java.util.logging.Level * and contains the corresponding cleanup code. */ -class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener { - - companion object { - fun cleanupAccounts(context: Context, db: AppDatabase) { - Logger.log.info("Cleaning up orphaned accounts") - - val accountNames = HashSet() - val accounts = AccountUtils.getMainAccounts(context) - - for (account in accounts.toTypedArray()) { - accountNames += account.name - } - - // delete orphaned address book accounts - val addressBookAccounts = AccountUtils.getAddressBookAccounts(context) - 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 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 == AccountManager.ACTION_AUTHENTICATOR_INTENT } - - override fun onAccountsUpdated(accounts: Array?) { - /* onAccountsUpdated may be called from the main thread, but cleanupAccounts - requires disk (database) access. So we launch it in a separate thread. */ - CoroutineScope(Dispatchers.Default).launch { - val db = EntryPointAccessors.fromApplication( - applicationContext, - AccountsUpdatedListener.AccountsUpdatedListenerEntryPoint::class.java - ).appDatabase() - - cleanupAccounts(applicationContext, db) - } - } - - 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.ACCOUNT_TYPE, - context.getString(R.string.google_account_type) - ) - - options?.let { - intent.putExtra(LoginActivity.OPEN_APP_PACKAGE_AFTER_AUTH, it.getString(LoginActivity.OPEN_APP_PACKAGE_AFTER_AUTH)) - intent.putExtra(LoginActivity.OPEN_APP_ACTIVITY_AFTER_AUTH, it.getString(LoginActivity.OPEN_APP_ACTIVITY_AFTER_AUTH)) - } - - 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 authStateString = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) ?: return null - - val authState = AuthState.jsonDeserialize(authStateString) - - if (authState != null) { - if (authState.needsTokenRefresh) { - val tokenRequest = authState.createTokenRefreshRequest() - val clientSecretString = accountManager.getUserData(account, AccountSettings.KEY_CLIENT_SECRET) - val clientSecret = OpenIdUtils.getClientAuthentication(clientSecretString) - - AuthorizationService(context).performTokenRequest(tokenRequest, clientSecret) { 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 - } -} +class GoogleAccountAuthenticatorService : DefaultAccountAuthenticatorService() diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAddressBookAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAddressBookAuthenticatorService.kt new file mode 100644 index 0000000000000000000000000000000000000000..47303605e98a8fb2ba73f12ec76b898ca2194478 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAddressBookAuthenticatorService.kt @@ -0,0 +1,20 @@ +/* + * Copyright ECORP SAS 2022 + * Copyright MURENA SAS 2023 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.syncadapter + +class GoogleAddressBookAuthenticatorService : DefaultAddressBookAuthenticatorService() \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAddressBooksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAddressBooksSyncAdapterService.kt index 152cc8a1b01589663d5baec8fc7a924f0bf76365..a79e47b2d9645ba8f0ce42e025b761c5d124d636 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAddressBooksSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAddressBooksSyncAdapterService.kt @@ -16,149 +16,4 @@ package at.bitfire.davdroid.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 at.bitfire.davdroid.HttpClient -import at.bitfire.davdroid.PermissionUtils -import at.bitfire.davdroid.closeCompat -import at.bitfire.davdroid.db.AppDatabase -import at.bitfire.davdroid.db.Collection -import at.bitfire.davdroid.db.Service -import at.bitfire.davdroid.log.Logger -import at.bitfire.davdroid.resource.LocalAddressBook -import at.bitfire.davdroid.settings.AccountSettings -import at.bitfire.davdroid.ui.account.AccountActivity -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import java.util.logging.Level - -class GoogleAddressBooksSyncAdapterService : SyncAdapterService() { - - override fun syncAdapter() = AddressBooksSyncAdapter(this, appDatabase) - - class AddressBooksSyncAdapter( - context: Context, - db: AppDatabase - ) : SyncAdapter(context, db) { - - override fun sync( - account: Account, - extras: Bundle, - authority: String, - httpClient: Lazy, - 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 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) - - PermissionUtils.notifyPermissions(context, 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 = addressBook.url.toHttpUrlOrNull()!! - 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, false) - } 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, db, contactsProvider, account, info, false) - } - } finally { - contactsProvider?.closeCompat() - } - - return true - } - - } - -} - +class GoogleAddressBooksSyncAdapterService : AddressBooksSyncAdapterService() diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleContactsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleContactsSyncAdapterService.kt index c15def3b6230ff8cc1d16794efab0a0a937cfb27..343a2ba3d9a5b78c9211f7009f4e78fad7c0af42 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleContactsSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleContactsSyncAdapterService.kt @@ -16,103 +16,4 @@ package at.bitfire.davdroid.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 at.bitfire.davdroid.HttpClient -import at.bitfire.davdroid.db.AppDatabase -import at.bitfire.davdroid.log.Logger -import at.bitfire.davdroid.resource.LocalAddressBook -import at.bitfire.davdroid.settings.AccountSettings -import java.util.logging.Level - -class GoogleContactsSyncAdapterService : SyncAdapterService() { - - companion object { - const val PREVIOUS_GROUP_METHOD = "previous_group_method" - } - - override fun syncAdapter() = ContactsSyncAdapter(this, appDatabase) - - class ContactsSyncAdapter( - context: Context, - db: AppDatabase - ) : SyncAdapter(context, db) { - - override fun sync( - account: Account, - extras: Bundle, - authority: String, - httpClient: Lazy, - 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, - httpClient.value, - extras, - authority, - syncResult, - provider, - addressBook - ).performSyncWithRetry() - } catch (e: Exception) { - Logger.log.log(Level.SEVERE, "Couldn't sync contacts", e) - } - Logger.log.info("Contacts sync complete") - } - } -} - +class GoogleContactsSyncAdapterService : ContactsSyncAdapterService() diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleNullAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleNullAuthenticatorService.kt deleted file mode 100644 index 0485928f26e3a4c4b0219d528064ca7e8262a8f5..0000000000000000000000000000000000000000 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleNullAuthenticatorService.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright ECORP SAS 2022 - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package at.bitfire.davdroid.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 at.bitfire.davdroid.ui.AccountsActivity - -class GoogleNullAuthenticatorService: 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/at/bitfire/davdroid/syncadapter/GoogleTasksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleTasksSyncAdapterService.kt index 082d3fd1d82531f948ccda137bb2f215eaf8b7bd..d885bf8addc148656f7c258c89f380ab63e36fb4 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleTasksSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleTasksSyncAdapterService.kt @@ -16,31 +16,6 @@ package at.bitfire.davdroid.syncadapter -import android.accounts.Account -import android.accounts.AccountManager -import android.content.ContentProviderClient -import android.content.ContentResolver -import android.content.Context -import android.content.SyncResult -import android.os.AsyncTask -import android.os.Build -import android.os.Bundle -import at.bitfire.davdroid.HttpClient -import at.bitfire.davdroid.db.AppDatabase -import at.bitfire.davdroid.db.Collection -import at.bitfire.davdroid.db.Credentials -import at.bitfire.davdroid.db.Service -import at.bitfire.davdroid.log.Logger -import at.bitfire.davdroid.resource.LocalTaskList -import at.bitfire.davdroid.settings.AccountSettings -import at.bitfire.ical4android.AndroidTaskList -import at.bitfire.ical4android.TaskProvider -import net.openid.appauth.AuthorizationService -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import org.dmfs.tasks.contract.TaskContract -import java.util.logging.Level - /** * Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}). */ diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncAdapterService.kt index a5afb7d8ba8a8e1bc4fd62d4e0fab3739c0b7be8..fb73d94621d030282b55bd9fe9d6ccedfe3cf12c 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncAdapterService.kt @@ -68,9 +68,7 @@ class JtxSyncAdapterService: SyncAdapterService() { val collections = JtxCollection.find(account, provider, context, LocalJtxCollection.Factory, null, null) for (collection in collections) { Logger.log.info("Synchronizing $collection") - JtxSyncManager(context, account, accountSettings, extras, httpClient.value, authority, syncResult, collection).let { - it.performSync() - } + JtxSyncManager(context, account, accountSettings, extras, httpClient.value, authority, syncResult, collection).performSync() } } catch (e: TaskProvider.ProviderTooOldException) { diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.kt index 4acce13cc47eee17d2da263d36d862b4f2ee6120..89dc5c0a189609a302cbf9bdefa9d6a7059a4646 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.kt @@ -25,6 +25,7 @@ import at.bitfire.dav4jvm.property.ScheduleTag import at.bitfire.dav4jvm.property.SyncToken import at.bitfire.davdroid.* import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.db.SyncState import at.bitfire.davdroid.db.SyncStats import at.bitfire.davdroid.log.Logger @@ -44,6 +45,8 @@ import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.* +import net.openid.appauth.AuthState +import net.openid.appauth.AuthorizationService import okhttp3.HttpUrl import okhttp3.RequestBody import org.apache.commons.io.FileUtils @@ -56,6 +59,7 @@ import java.net.ConnectException import java.net.HttpURLConnection import java.security.cert.CertificateException import java.util.* +import java.util.concurrent.Executors import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit @@ -75,6 +79,8 @@ abstract class SyncManager, out CollectionType: L val localCollection: CollectionType ) { + private val executor = Executors.newSingleThreadExecutor() + @EntryPoint @InstallIn(SingletonComponent::class) interface SyncManagerEntryPoint { @@ -90,6 +96,10 @@ abstract class SyncManager, out CollectionType: L const val DEBUG_INFO_MAX_RESOURCE_DUMP_SIZE = 100*FileUtils.ONE_KB.toInt() const val MAX_MULTIGET_RESOURCES = 10 + const val DEFAULT_RETRY_AFTER = 5 + const val DEFAULT_SECOND_RETRY_AFTER = 8 + const val DEFAULT_MAX_RETRY_TIME = 21 + var _workDispatcher: WeakReference? = null /** * We use our own dispatcher to @@ -135,8 +145,48 @@ abstract class SyncManager, out CollectionType: L /** * Call performSync with default retry values */ - fun performSyncWithRetry() { - performSync(5, 8, 21) + fun performSync() { + val authState = accountSettings.credentials().authState + + if (authState == null || !authState.needsTokenRefresh) { + performSync(DEFAULT_RETRY_AFTER, DEFAULT_SECOND_RETRY_AFTER, DEFAULT_MAX_RETRY_TIME) + return + } + + refreshAuthTokenAndSync(authState) + } + + private fun refreshAuthTokenAndSync(authState: AuthState) { + val tokenRequest = authState.createTokenRefreshRequest() + val clientSecretString = accountSettings.credentials().clientSecret + val clientSecret = OpenIdUtils.getClientAuthentication(clientSecretString) + + AuthorizationService(context).performTokenRequest(tokenRequest, clientSecret) { tokenResponse, ex -> + authState.update(tokenResponse, ex) + accountSettings.credentials( + Credentials( + account.name, + null, + authState, + null, + clientSecret = clientSecretString + ) + ) + + accountSettings.credentials( + Credentials( + account.name, + null, + authState, + null, + clientSecret = clientSecretString + ) + ) + + executor.execute { + performSync(DEFAULT_RETRY_AFTER, DEFAULT_SECOND_RETRY_AFTER, DEFAULT_MAX_RETRY_TIME) + } + } } /** @@ -146,7 +196,7 @@ abstract class SyncManager, out CollectionType: L * @param secondRetryAfter optional param, in seconds. Used to calculate fibonnacci sequence for rety on unhandled exception * @param maxRetryTime optional param, in seconds. On unhandled exception, max time the method should retry. */ - fun performSync(retryAfter: Int = Int.MIN_VALUE, secondRetryAfter: Int = Int.MIN_VALUE, maxRetryTime: Int = Int.MIN_VALUE) { + fun performSync(retryAfter: Int, secondRetryAfter: Int, maxRetryTime: Int) { // dismiss previous error notifications notificationManager.cancel(notificationTag, NotificationUtils.NOTIFY_SYNC_ERROR) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt index 7aa0c8712f557e3410c09ffec818eb008c2097bb..c9d10c372916e4488c3592730f492f65935953ea 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt @@ -73,49 +73,7 @@ open class TasksSyncAdapterService: SyncAdapterService() { .sortedByDescending { priorityTaskLists.contains(it.id) } for (taskList in taskLists) { Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]") - TasksSyncManager(context, account, accountSettings, httpClient.value, extras, authority, syncResult, taskList).let { - val authState = accountSettings.credentials().authState - - if (authState != null) { - if (authState.needsTokenRefresh) { - val tokenRequest = authState.createTokenRefreshRequest() - val clientSecretString = accountSettings.credentials().clientSecret - val clientSecret = OpenIdUtils.getClientAuthentication(clientSecretString) - - AuthorizationService(context).performTokenRequest(tokenRequest, clientSecret) { tokenResponse, ex -> - authState.update(tokenResponse, ex) - accountSettings.credentials( - Credentials( - account.name, - null, - authState, - null, - clientSecret = clientSecretString - ) - ) - it.accountSettings.credentials( - Credentials( - it.account.name, - null, - authState, - null, - clientSecret = clientSecretString - ) - ) - object : AsyncTask() { - override fun doInBackground(vararg params: Void): Void? { - it.performSyncWithRetry() - return null - } - }.execute() - } - } else { - it.performSyncWithRetry() - } - } else { - it.performSyncWithRetry() - } - } + TasksSyncManager(context, account, accountSettings, httpClient.value, extras, authority, syncResult, taskList).performSync() } } catch (e: TaskProvider.ProviderTooOldException) { SyncUtils.notifyProviderTooOld(context, e) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooAccountAuthenticatorService.kt new file mode 100644 index 0000000000000000000000000000000000000000..aa0ab4f891c0984c3864a7ae101f00750e93b721 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooAccountAuthenticatorService.kt @@ -0,0 +1,19 @@ +/* + * Copyright MURENA SAS 2023 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.syncadapter + +class YahooAccountAuthenticatorService : DefaultAccountAuthenticatorService() diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooAddressBookAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooAddressBookAuthenticatorService.kt new file mode 100644 index 0000000000000000000000000000000000000000..24ec3a4d0c242d2aa979b8c52ff55cb007cda8ab --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooAddressBookAuthenticatorService.kt @@ -0,0 +1,19 @@ +/* + * Copyright MURENA SAS 2023 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.syncadapter + +class YahooAddressBookAuthenticatorService : DefaultAddressBookAuthenticatorService() diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooAddressBooksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooAddressBooksSyncAdapterService.kt new file mode 100644 index 0000000000000000000000000000000000000000..623b893671730a884c0af875ef6d49169a3b7043 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooAddressBooksSyncAdapterService.kt @@ -0,0 +1,19 @@ +/* + * Copyright MURENA SAS 2023 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.syncadapter + +class YahooAddressBooksSyncAdapterService : AddressBooksSyncAdapterService() \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooCalendarsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooCalendarsSyncAdapterService.kt new file mode 100644 index 0000000000000000000000000000000000000000..ee69f8a76e8187f3d3ec4d73c6e173f44ac4c38a --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooCalendarsSyncAdapterService.kt @@ -0,0 +1,19 @@ +/* + * Copyright MURENA SAS 2023 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.syncadapter + +class YahooCalendarsSyncAdapterService : CalendarsSyncAdapterService() diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooContactsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooContactsSyncAdapterService.kt new file mode 100644 index 0000000000000000000000000000000000000000..9954da15ba7f46cc94a8aad9b6864d46cd465633 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooContactsSyncAdapterService.kt @@ -0,0 +1,19 @@ +/* + * Copyright MURENA SAS 2023 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.syncadapter + +class YahooContactsSyncAdapterService : ContactsSyncAdapterService() \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooEmailSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooEmailSyncAdapterService.kt new file mode 100644 index 0000000000000000000000000000000000000000..0c59b72bcc5f9a5a914145c60c2c12da79a8acf8 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooEmailSyncAdapterService.kt @@ -0,0 +1,19 @@ +/* + * Copyright MURENA SAS 2023 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.syncadapter + +class YahooEmailSyncAdapterService : EmailSyncAdapterService() diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooTasksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooTasksSyncAdapterService.kt new file mode 100644 index 0000000000000000000000000000000000000000..f48a51507f11b311b4d34d62060c08249af0504b --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/YahooTasksSyncAdapterService.kt @@ -0,0 +1,19 @@ +/* + * Copyright MURENA SAS 2023 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.syncadapter + +class YahooTasksSyncAdapterService : TasksSyncAdapterService() diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/CreateCollectionFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/CreateCollectionFragment.kt index 39ac27cd6d2d4d6fb1d70bea27c39a35e45290ea..756f22b766e6841d7cf391b7cd36de7070b0c60c 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/account/CreateCollectionFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/account/CreateCollectionFragment.kt @@ -133,11 +133,12 @@ class CreateCollectionFragment: DialogFragment() { fun createCollection(): LiveData { viewModelScope.launch(Dispatchers.IO + NonCancellable) { - HttpClient.Builder(context, AccountSettings(context, account)) + val accountSettings = AccountSettings(context, account) + HttpClient.Builder(context, accountSettings) .setForeground(true) .build().use { httpClient -> try { - val dav = DavResource(httpClient.okHttpClient, collection.url) + val dav = DavResource(httpClient.okHttpClient, collection.url, accountSettings.credentials().authState?.accessToken) // create collection on remote server dav.mkCol(generateXml()) {} diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/DeleteCollectionFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/DeleteCollectionFragment.kt index cfcec86c506b43f595ba2e432cec8a23103ce52b..c9245f315aa0d2d68e575d1b76bae1b192e94243 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/account/DeleteCollectionFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/account/DeleteCollectionFragment.kt @@ -114,11 +114,13 @@ class DeleteCollectionFragment: DialogFragment() { viewModelScope.launch(Dispatchers.IO + NonCancellable) { val collectionInfo = collectionInfo ?: return@launch - HttpClient.Builder(context, AccountSettings(context, account)) + val accountSettings = AccountSettings(context, account) + + HttpClient.Builder(context, accountSettings) .setForeground(true) .build().use { httpClient -> try { - val collection = DavResource(httpClient.okHttpClient, collectionInfo.url) + val collection = DavResource(httpClient.okHttpClient, collectionInfo.url, accountSettings.credentials().authState?.accessToken) // delete collection from server collection.delete(null) {} diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index dff662c4303289eb1d1074d44b587d10c9cd5494..73167bbfd059c6b08d007274b6f61ec7bc932275 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -252,6 +252,11 @@ class AccountDetailsFragment : Fragment() { addressBookAccountType = context.getString(R.string.account_type_google_address_book) baseURL = null } + context.getString(R.string.yahoo_account_type) -> { + accountType = context.getString(R.string.yahoo_account_type) + addressBookAccountType = context.getString(R.string.account_type_yahoo_address_book) + baseURL = null + } } val account = Account(credentials?.userName, accountType) @@ -263,12 +268,11 @@ class AccountDetailsFragment : Fragment() { val accountManager = AccountManager.get(context) if (!AccountUtils.createAccount(context, account, userData, credentials?.password)) { - if (accountType == context.getString(R.string.google_account_type)) { - for (googleAccount in accountManager.getAccountsByType(context.getString( - R.string.google_account_type))) { + if (accountType in AccountUtils.getOpenIdMainAccountTypes(context)) { + for (openIdAccount in AccountUtils.getOpenIdMainAccounts(context)) { if (userData.get(AccountSettings.KEY_EMAIL_ADDRESS) == accountManager .getUserData(account, AccountSettings.KEY_EMAIL_ADDRESS)) { - accountManager.setUserData(googleAccount, AccountSettings.KEY_AUTH_STATE, + accountManager.setUserData(openIdAccount, AccountSettings.KEY_AUTH_STATE, userData.getString(AccountSettings.KEY_AUTH_STATE)) } } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthFragment.kt index ef16ce84141d73331b7770613a462cb7bd8de43c..b6686ff3bdf1f1d5162b1124ce14d537b06f94c8 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthFragment.kt @@ -60,7 +60,7 @@ class GoogleAuthFragment : OpenIdAuthenticationBaseFragment(IdentityProvider.GOO } private fun showConfirmationDialog() { - val title = SpannableString(getString(R.string.google_alert_title)) + val title = SpannableString(getString(R.string.warning)) title.setSpan( AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt index 9d1a8e3dfe640f3cad673c8719036de5e4218068..8bbd99bf090d8cf7322c1a5dad908bb65b8cd561 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt @@ -38,6 +38,7 @@ class LoginActivity: AppCompatActivity() { */ const val EXTRA_PASSWORD = "password" + const val AUTH_STATE = "authState" const val ACCOUNT_TYPE = "account_type" const val OPENID_AUTH_FLOW_COMPLETE = "openId_authFlow_complete" diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/MurenaLoginFragmentFactory.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/MurenaLoginFragmentFactory.kt index b87f7447900ed92b81cfddd9766f475b09d3a0f4..3a2462e932083d00fea25a2691a59dc26bc8cd8d 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/MurenaLoginFragmentFactory.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/MurenaLoginFragmentFactory.kt @@ -37,6 +37,7 @@ class MurenaLoginFragmentFactory @Inject constructor(@ApplicationContext val con return when (accountType) { context.getString(R.string.eelo_account_type) -> EeloAuthenticatorFragment() context.getString(R.string.google_account_type) -> GoogleAuthFragment() + context.getString(R.string.yahoo_account_type) -> YahooAuthFragment() else -> null } } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthStateSetupState.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthStateSetupState.kt new file mode 100644 index 0000000000000000000000000000000000000000..0d4174d2169164b6c385021e75c4d3a0f55913ba --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthStateSetupState.kt @@ -0,0 +1,23 @@ +/* + * Copyright MURENA SAS 2023 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package at.bitfire.davdroid.ui.setup + +enum class OpenIdAuthStateSetupState { + ALREADY_SET_UP, + SET_UP_FAILED, + SET_UP_SUCCEED +} diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationBaseFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationBaseFragment.kt index e4808648f6b1fd2ffdc0aa0e7ff92b121e2a2a18..9d640a2fb0c6fb90823b758d3d1536eb55f56cf7 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationBaseFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationBaseFragment.kt @@ -16,13 +16,12 @@ package at.bitfire.davdroid.ui.setup +import android.app.Activity import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels @@ -49,23 +48,6 @@ abstract class OpenIdAuthenticationBaseFragment(private val identityProvider: Id private val viewModel by viewModels() private val loginModel by activityViewModels() - private val authReqActivityResultLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - val intent = result.data - intent?.let { - val response = AuthorizationResponse.fromIntent(it) - val exception = AuthorizationException.fromIntent(it) - - if (response == null || exception != null) { - Logger.log.log(Level.SEVERE, "Failed to retrieve auth response", exception) - handleLoginFailedToast() - return@let - } - - performTokenRequest(response, exception) - } - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -76,6 +58,36 @@ abstract class OpenIdAuthenticationBaseFragment(private val identityProvider: Id return inflater.inflate(R.layout.frament_openid_auth, container, false) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (isAuthFlowComplete()) { + val setupState = viewModel.setUpAuthState(requireActivity().intent) + if (setupState == OpenIdAuthStateSetupState.ALREADY_SET_UP) { + return + } + + if (setupState == OpenIdAuthStateSetupState.SET_UP_FAILED) { + handleLoginFailedToast() + return + } + + extractAuthCodeResponse() + } + } + + private fun extractAuthCodeResponse() { + val response = AuthorizationResponse.fromIntent(requireActivity().intent) + val exception = AuthorizationException.fromIntent(requireActivity().intent) + + if (response == null || exception != null) { + Logger.log.log(Level.SEVERE, "Failed to retrieve auth response", exception) + handleLoginFailedToast() + return + } + + performTokenRequest(response, exception) + } + protected fun isAuthFlowComplete(): Boolean { return activity?.intent?.getBooleanExtra( LoginActivity.OPENID_AUTH_FLOW_COMPLETE, @@ -108,8 +120,9 @@ abstract class OpenIdAuthenticationBaseFragment(private val identityProvider: Id return } - val authIntent = viewModel.getAuthIntent(serviceConfiguration) - authReqActivityResultLauncher.launch(authIntent) + viewModel.requestAuthCode(serviceConfiguration, requireActivity().intent) + requireActivity().setResult(Activity.RESULT_OK) + requireActivity().finish() } private fun performTokenRequest( diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationViewModel.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationViewModel.kt index f29cec9369fe0f1a59cc462ead2cee87f30c067a..f740d8c20609d34cb4f09d312d8300d74a255d51 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationViewModel.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationViewModel.kt @@ -17,14 +17,24 @@ package at.bitfire.davdroid.ui.setup import android.app.Application +import android.app.PendingIntent import android.content.Intent +import android.os.Build import androidx.annotation.WorkerThread import androidx.lifecycle.AndroidViewModel import at.bitfire.davdroid.authorization.IdentityProvider import at.bitfire.davdroid.log.Logger -import net.openid.appauth.* +import net.openid.appauth.AuthState import net.openid.appauth.AuthState.AuthStateAction +import net.openid.appauth.AuthorizationException +import net.openid.appauth.AuthorizationRequest +import net.openid.appauth.AuthorizationResponse +import net.openid.appauth.AuthorizationService import net.openid.appauth.AuthorizationService.TokenResponseCallback +import net.openid.appauth.AuthorizationServiceConfiguration +import net.openid.appauth.ClientSecretBasic +import net.openid.appauth.ResponseTypeValues +import net.openid.appauth.TokenResponse import okio.buffer import okio.source import org.json.JSONObject @@ -48,19 +58,69 @@ class OpenIdAuthenticationViewModel(application: Application) : AndroidViewModel return authState!! } - fun getAuthIntent(serviceConfiguration: AuthorizationServiceConfiguration): Intent { + fun setUpAuthState(intent: Intent): OpenIdAuthStateSetupState { + if (authState != null) { + return OpenIdAuthStateSetupState.ALREADY_SET_UP + } + + val authStateString = + intent.getStringExtra(LoginActivity.AUTH_STATE) ?: return OpenIdAuthStateSetupState.SET_UP_FAILED + + authState = AuthState.jsonDeserialize(authStateString) + + if (authState == null) { + return OpenIdAuthStateSetupState.SET_UP_FAILED + } + + return OpenIdAuthStateSetupState.SET_UP_SUCCEED + } + + fun requestAuthCode(serviceConfiguration: AuthorizationServiceConfiguration, intent: Intent) { authState = AuthState(serviceConfiguration) - val authRequestBuilder = AuthorizationRequest.Builder( + val authRequest = AuthorizationRequest.Builder( serviceConfiguration, identityProvider!!.clientId, ResponseTypeValues.CODE, identityProvider!!.redirectUri + ).setScope(identityProvider!!.scope) + .build() + + authorizationService.performAuthorizationRequest( + authRequest, + createPostAuthorizationIntent(authRequest, intent), + authorizationService.createCustomTabsIntentBuilder().build() + ) + } + + private fun createPostAuthorizationIntent( + request: AuthorizationRequest, + providedIntent: Intent + ): PendingIntent { + val intent = Intent(getApplication(), LoginActivity::class.java) + + intent.putExtra(LoginActivity.AUTH_STATE, authState?.jsonSerializeString()) + + intent.putExtra( + LoginActivity.ACCOUNT_TYPE, + providedIntent.getStringExtra(LoginActivity.ACCOUNT_TYPE) + ) + intent.putExtra(LoginActivity.OPENID_AUTH_FLOW_COMPLETE, true) + intent.putExtra( + LoginActivity.OPEN_APP_PACKAGE_AFTER_AUTH, + providedIntent.getStringExtra(LoginActivity.OPEN_APP_PACKAGE_AFTER_AUTH) + ) + intent.putExtra( + LoginActivity.OPEN_APP_ACTIVITY_AFTER_AUTH, + providedIntent.getStringExtra(LoginActivity.OPEN_APP_ACTIVITY_AFTER_AUTH) ) - authRequestBuilder.setScopes(identityProvider!!.scope) + var flag = 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flag = flag or PendingIntent.FLAG_MUTABLE + } - return authorizationService.getAuthorizationRequestIntent(authRequestBuilder.build()) + return PendingIntent.getActivity(getApplication(), request.hashCode(), intent, flag) } fun performTokenRequest( @@ -102,7 +162,8 @@ class OpenIdAuthenticationViewModel(application: Application) : AndroidViewModel var infoEndpoint = identityProvider?.userInfoEndpoint if (infoEndpoint == null) { - val discovery = authState?.authorizationServiceConfiguration?.discoveryDoc ?: return null + val discovery = + authState?.authorizationServiceConfiguration?.discoveryDoc ?: return null infoEndpoint = discovery.userinfoEndpoint.toString() } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/YahooAuthFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/YahooAuthFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..250189f1eeda04c0317b33222081a3e7d330d82c --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/YahooAuthFragment.kt @@ -0,0 +1,64 @@ +package at.bitfire.davdroid.ui.setup + +import android.os.Bundle +import android.text.Layout +import android.text.SpannableString +import android.text.style.AlignmentSpan +import android.view.View +import at.bitfire.davdroid.R +import at.bitfire.davdroid.authorization.IdentityProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.json.JSONObject + +class YahooAuthFragment : OpenIdAuthenticationBaseFragment(IdentityProvider.YAHOO) { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + handleConfirmationDialog() + } + + override fun onAuthenticationComplete(userData: JSONObject) { + val emailKey = "email" + + if (!userData.has(emailKey)) { + handleLoginFailedToast() + return + } + + val email = userData.getString(emailKey) + if (email.isBlank()) { + handleLoginFailedToast() + return + } + + val calDavUrl = "https://caldav.calendar.yahoo.com/dav/$email" + val cardDavUrl = "https://carddav.address.yahoo.com/dav/$email" + proceedNext(email, calDavUrl, cardDavUrl) + } + + private fun handleConfirmationDialog() { + if (isAuthFlowComplete()) { + return + } + + showConfirmationDialog() + } + + private fun showConfirmationDialog() { + val title = SpannableString(getString(R.string.warning)) + title.setSpan( + AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), + 0, + title.length, + 0 + ) + + MaterialAlertDialogBuilder(requireContext(), R.style.CustomAlertDialogStyle) + .setTitle(title) + .setMessage(R.string.yahoo_alert_message) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> + startAuthFLow() + }.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt index 45db38adf282ff7a4823ca2430c9977920c637e7..6c1f00544f0e59c1841d08210934e726558648c9 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/webdav/AddWebdavMountActivity.kt @@ -174,7 +174,7 @@ class AddWebdavMountActivity: AppCompatActivity() { fun hasWebDav(mount: WebDavMount, credentials: Credentials?): Boolean { var supported = false HttpClient.Builder(context, null, credentials).build().use { client -> - val dav = DavResource(client.okHttpClient, mount.url) + val dav = DavResource(client.okHttpClient, mount.url, credentials?.authState?.accessToken) dav.options { davCapabilities, _ -> if (CollectionUtils.containsAny(davCapabilities, "1", "2", "3")) supported = true diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt b/app/src/main/java/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt index 1a0aab256d1b9f6fab8fc3f42f3a82b9ee14e613..6ca88fd78ed76508d48bdd9cc84e1e9556b4f073 100644 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt @@ -326,7 +326,9 @@ class DavDocumentsProvider: DocumentsProvider() { val dstDocId: String httpClient(srcDoc.mountId).use { client -> - val dav = DavResource(client.okHttpClient, srcDoc.toHttpUrl(db)) + val accessToken = credentialsStore.getCredentials(srcDoc.mountId)?.authState?.accessToken + + val dav = DavResource(client.okHttpClient, srcDoc.toHttpUrl(db), accessToken) try { val dstUrl = dstFolder.toHttpUrl(db).newBuilder() .addPathSegment(name) @@ -369,7 +371,10 @@ class DavDocumentsProvider: DocumentsProvider() { val newLocation = parentUrl.newBuilder() .addPathSegment(newName) .build() - val doc = DavResource(client.okHttpClient, newLocation) + + val accessToken = credentialsStore.getCredentials(parent.mountId)?.authState?.accessToken + + val doc = DavResource(client.okHttpClient, newLocation, accessToken) try { if (createDirectory) doc.mkCol(null) { @@ -403,7 +408,9 @@ class DavDocumentsProvider: DocumentsProvider() { Logger.log.fine("WebDAV removeDocument $documentId") val doc = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException() httpClient(doc.mountId).use { client -> - val dav = DavResource(client.okHttpClient, doc.toHttpUrl(db)) + val accessToken = credentialsStore.getCredentials(doc.mountId)?.authState?.accessToken + + val dav = DavResource(client.okHttpClient, doc.toHttpUrl(db), accessToken) try { dav.delete { // successfully deleted @@ -431,7 +438,9 @@ class DavDocumentsProvider: DocumentsProvider() { .build() httpClient(doc.mountId).use { client -> - val dav = DavResource(client.okHttpClient, doc.toHttpUrl(db)) + val accessToken = credentialsStore.getCredentials(doc.mountId)?.authState?.accessToken + + val dav = DavResource(client.okHttpClient, doc.toHttpUrl(db), accessToken) try { dav.move(newLocation, false) { // successfully moved @@ -462,7 +471,9 @@ class DavDocumentsProvider: DocumentsProvider() { .addPathSegment(newName) .build() try { - val dav = DavResource(client.okHttpClient, oldUrl) + val accessToken = credentialsStore.getCredentials(doc.mountId)?.authState?.accessToken + + val dav = DavResource(client.okHttpClient, oldUrl, accessToken) dav.move(newLocation, false) { // successfully renamed } @@ -511,7 +522,9 @@ class DavDocumentsProvider: DocumentsProvider() { } val fileInfo = headResponseCache.get(doc) { - val deferredFileInfo = executor.submit(HeadInfoDownloader(client, url)) + val accessToken = credentialsStore.getCredentials(doc.mountId)?.authState?.accessToken + + val deferredFileInfo = executor.submit(HeadInfoDownloader(client, url, accessToken)) signal?.setOnCancelListener { deferredFileInfo.cancel(true) } @@ -526,10 +539,14 @@ class DavDocumentsProvider: DocumentsProvider() { (fileInfo.eTag != null || fileInfo.lastModified != null) && // we need a method to determine whether the document has changed during access fileInfo.supportsPartial != false // WebDAV server must support random access ) { - val accessor = RandomAccessCallback.Wrapper(ourContext, client, url, doc.mimeType, fileInfo, signal) + val accessToken = credentialsStore.getCredentials(doc.mountId)?.authState?.accessToken + + val accessor = RandomAccessCallback.Wrapper(ourContext, client, url, doc.mimeType, fileInfo, signal, accessToken) storageManager.openProxyFileDescriptor(modeFlags, accessor, accessor.callback!!.workerHandler) } else { - val fd = StreamingFileDescriptor(ourContext, client, url, doc.mimeType, signal) { transferred -> + val accessToken = credentialsStore.getCredentials(doc.mountId)?.authState?.accessToken + + val fd = StreamingFileDescriptor(ourContext, client, url, doc.mimeType, signal, accessToken) { transferred -> // called when transfer is finished val now = System.currentTimeMillis() @@ -567,9 +584,12 @@ class DavDocumentsProvider: DocumentsProvider() { val thumbFile = thumbnailCache.get(doc, sizeHint) { // create thumbnail val result = executor.submit(Callable { + httpClient(doc.mountId).use { client -> + val accessToken = credentialsStore.getCredentials(doc.mountId)?.authState?.accessToken + val url = doc.toHttpUrl(db) - val dav = DavResource(client.okHttpClient, url) + val dav = DavResource(client.okHttpClient, url, accessToken) var result: ByteArray? = null dav.get("image/*", null) { response -> response.body?.byteStream()?.use { data -> diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/HeadInfoDownloader.kt b/app/src/main/java/at/bitfire/davdroid/webdav/HeadInfoDownloader.kt index 53cd439519541d0bc2504e331923734fa6cf78a6..7e499d4984444d6ffd4b8c4120cb7008f339d753 100644 --- a/app/src/main/java/at/bitfire/davdroid/webdav/HeadInfoDownloader.kt +++ b/app/src/main/java/at/bitfire/davdroid/webdav/HeadInfoDownloader.kt @@ -14,7 +14,8 @@ import java.util.concurrent.Callable class HeadInfoDownloader( val client: HttpClient, - val url: HttpUrl + val url: HttpUrl, + val accessToken: String? = null ): Callable { override fun call(): HeadResponse { @@ -23,7 +24,7 @@ class HeadInfoDownloader( var lastModified: Date? = null var supportsPartial: Boolean? = null - DavResource(client.okHttpClient, url).head { response -> + DavResource(client.okHttpClient, url, accessToken).head { response -> response.header("ETag", null)?.let { eTag = QuotedStringUtils.decodeQuotedString(it.removeSuffix("W/")) } diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/RandomAccessCallback.kt b/app/src/main/java/at/bitfire/davdroid/webdav/RandomAccessCallback.kt index e0406f79575adbcfeb23507e213a2985c55fe6a0..5cb33ee8583810699d86cf09dd2c2d673d680a22 100644 --- a/app/src/main/java/at/bitfire/davdroid/webdav/RandomAccessCallback.kt +++ b/app/src/main/java/at/bitfire/davdroid/webdav/RandomAccessCallback.kt @@ -43,7 +43,8 @@ class RandomAccessCallback private constructor( val url: HttpUrl, val mimeType: MediaType?, val headResponse: HeadResponse, - val cancellationSignal: CancellationSignal? + val cancellationSignal: CancellationSignal?, + val accessToken: String? = null ): ProxyFileDescriptorCallback(), SegmentedCache.PageLoader { companion object { @@ -70,7 +71,7 @@ class RandomAccessCallback private constructor( } - private val dav = DavResource(httpClient.okHttpClient, url) + private val dav = DavResource(httpClient.okHttpClient, url, accessToken) private val fileSize = headResponse.size ?: throw IllegalArgumentException("Can only be used with given file size") private val documentState = headResponse.toDocumentState() ?: throw IllegalArgumentException("Can only be used with ETag/Last-Modified") @@ -209,10 +210,11 @@ class RandomAccessCallback private constructor( url: HttpUrl, mimeType: MediaType?, headResponse: HeadResponse, - cancellationSignal: CancellationSignal? + cancellationSignal: CancellationSignal?, + accessToken: String? = null ): ProxyFileDescriptorCallback() { - var callback: RandomAccessCallback? = RandomAccessCallback(context, httpClient, url, mimeType, headResponse, cancellationSignal) + var callback: RandomAccessCallback? = RandomAccessCallback(context, httpClient, url, mimeType, headResponse, cancellationSignal, accessToken) override fun onFsync() = callback?.onFsync() ?: throw IllegalStateException("Must not be called after onRelease()") diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt b/app/src/main/java/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt index 4c91972eeb8ce5696891e0af61c96fbf3c04be9d..2b9cb19ea5aca410213fad5c9a48c666dc0a1539 100644 --- a/app/src/main/java/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt +++ b/app/src/main/java/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt @@ -33,6 +33,7 @@ class StreamingFileDescriptor( val url: HttpUrl, val mimeType: MediaType?, val cancellationSignal: CancellationSignal?, + val accessToken: String? = null, val finishedCallback: OnSuccessCallback ) { @@ -41,7 +42,7 @@ class StreamingFileDescriptor( private const val BUFFER_SIZE = FileUtils.ONE_MB.toInt() } - val dav = DavResource(client.okHttpClient, url) + val dav = DavResource(client.okHttpClient, url, accessToken) var transferred: Long = 0 private val notificationManager = NotificationManagerCompat.from(context) @@ -180,7 +181,7 @@ class StreamingFileDescriptor( } } } - DavResource(client.okHttpClient, url).put(body) { + DavResource(client.okHttpClient, url, accessToken).put(body) { // upload successful } } diff --git a/app/src/main/res/drawable/ic_account_provider_yahoo.xml b/app/src/main/res/drawable/ic_account_provider_yahoo.xml new file mode 100644 index 0000000000000000000000000000000000000000..fb2ee79a96eb7efe48076c9d7994895f3623428f --- /dev/null +++ b/app/src/main/res/drawable/ic_account_provider_yahoo.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index c454563b59834162a7bb49c1eaf44a0b102355a7..6c940f02f8ed6354957c5fb475d9eac0dd6cb647 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -249,5 +249,5 @@ Murena.io Google WebDAV - ВНИМАНИЕ + ВНИМАНИЕ diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4333fa6208045105bf1a52dd2eb24dcba710a3f2..52e358a3d994cf54ea2f0b021aa2d2c47725a18c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -272,7 +272,7 @@ Google Murena wird Google ein falsches Gerätemodell angeben, um Ihre Privatsphäre zu schützen. \nSie können es unter \"Meine Geräte\" auf Google überprüfen, nachdem Sie sich angemeldet haben. - WARNUNG + WARNUNG Benutzernamen und Passwort aktualisieren Anmeldedaten Einen bestimmten Server benutzen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ab279327d7657b5317580e270b8ae6fba3b049c8..a6382050323678359a56ff1a37c7bf0c7c036855 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -279,7 +279,7 @@ Si se deben crear recordatorios para eventos predeterminados sin recordatorio: el número deseado de minutos antes del evento. Déjelo en blanco para deshabilitar los recordatorios predeterminados. Murena reportará un dispositivo falso a Google para proteger su privacidad. \nPuedes revisar cuáles en \"Tus dispositivos\" de Google después de iniciar sesión. - ADVERTENCIA + ADVERTENCIA Crear Cuenta Todos los permisos concedidos Utilice esta opción para activar todas las funciones (recomendado) diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index e5a1efeb1e8882075197cd87593d4f6706ef87b6..a26a38cfaa31eacc62025037978f18e843c314fa 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -154,7 +154,7 @@ Sistemako eta erabiltzaileak gehitutako ziurtagiriak ez dira fidagarritzat joko Itxi nabigazio-panel lerrakorra Ireki nabigazio-panel lerrakorra - ABISUA + ABISUA Zure gailuak Kontu-kudeatzailearen sinkronizazioa kontrolatuko du. Kontu-kudeatzailearen sinkronizazio-tarte erregularrak betearazteko, itzali \"bateriaren optimizazioa\". Egutegi/Kontaktu sinkronizazio moldagailua Orain %s jarduera guztiak erregistratzen ari dira diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 816c37fc59717423fb012440b524e58ba6263b55..f323e5f8d727e8ae65f4239034edba2cfb4b7d39 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -259,7 +259,7 @@ Gestionnaire de compte a rencontré un certificat inconnu. Voulez-vous lui faire confiance \? Murena déclarera un faux modèle d\'appareil à Google afin de protéger votre vie privée. \nVous pouvez vérifier lequel sur l\'Activité des appareils de Google après vous être connecté. - AVERTISSEMENT + AVERTISSEMENT Si des rappels par défaut sont créés pour les événements sans rappel : le nombre de minutes souhaité avant l\'événement. Laissez vide pour désactiver les rappels par défaut. Aucun rappel par défaut n\'est créé diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 362c8c0e06bfe459eb61d582f6051bf03c0a43ba..823bfc94a8c20b5e1bb339b6530cd1e96440025d 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -221,7 +221,7 @@ Manaidsear nan cunntas Nì Murena aithriseadh le modail uidheim brèige gu Google airson do phrìobhaideachd a dhìon. \n’S urrainn dhut sùil a thoirt air dè th’ ann air “Gnìomhachd an uidheim” Google às dèidh do chlàradh a-steach. - RABHADH + RABHADH Thachair manaidsear nan cunntas ri teisteanas nach aithne dhuinn. A bheil thu airson earbsa a chur ann\? Manaidsear nan cunntas: Tèarainteachd a’ cheangail A’ leigeil seachad goireas mì-dhligheach no dhà diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 67c2e6395053f230028a6707e182c41614f1e3d3..b05d4a7137c33d92b8ff607e6598f44492e15303 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -258,7 +258,7 @@ WebDAV Murena informará sobre un modelo de dispositivo falso a Google para protexer a súa privacidade. \nPode comprobar cal en Actividade do dispositivo de Google despois de acceder. - Aviso + Aviso Se os recordatorios perdeterminados se deben crear para eventos sen recordatorio: a cantidade de minutos desexados antes do evento. Déixeo en branco para desactivar os recordatorios predeterminados. Non se crean recordatorios predeterminados diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 6c6313ff886b4ade77498f78856171272016123c..7207e8058d6d57dfd87c9a46e3f63c58ff6b936d 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -275,7 +275,7 @@ Nem készült alapértelmezett értesítő Naplókat csatoltak ehhez az üzenethez. A fogadó alkalmazás részéről szükséges a csatolmányok támogatása. - FIGYELMEZTETÉS + FIGYELMEZTETÉS Az Murena egy hamis készülékmodellt for jelenteni a Google számára, az adatvédelmed érdekében. \nEllenőrizheted, hogy melyik modell van a Google készülékaktivitásban, miután beléptél. diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 9c4875c6a28fc6394c01e2f6231303da3cabdc74..86bda71dc9756f11683baa8998f666e14c1d1dd9 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -106,7 +106,7 @@ %s of gamalt Reyna aftur Skoða atriði - AÐVÖRUN + AÐVÖRUN Samstilling Villur í samstillingu Aðvaranir vegna samstillingar diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f06c9e191b9ded43a4e2b2148cbae0acc48252ba..0ea25b468dc3a8fdf1e0c1320a879a7eacd262cd 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -231,7 +231,7 @@ Informativa sulla privacy Murena invierà un modello di dispositivo fasullo a Google per proteggere la tua privacy. \nPuoi vedere quale su Attività Dispositivi di Google dopo aver effettuato l\'accesso. - ATTENZIONE + ATTENZIONE Sto ignorando una o più risorse non valide Ricevuta attività non valida dal server Ricevuto evento non valido dal server diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 37e433b0cef534fe501e4ee070e16d532e7e7487..972f446890c6b4d8cb9a2518836c317c6afb290b 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -266,7 +266,7 @@ WebDAV Murena zal een nep apparaat model aan Google rapporteren om jouw privacy te beschermen. \nJe kan bekijken welk model op Toestel Activiteit van Google nadat je bent ingelogd. - WAARSCHUWING + WAARSCHUWING Geen standaard herinneringen aangemaakt Standaard herinnering één minuut voor het evenement diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ef73e31cd92bb6358345ab8fc9026d29ae951383..ff44437a469fbe85e567d3cdec527b037a89c4b9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -274,7 +274,7 @@ Менеджер учётных записей обнаружил неизвестный сертификат. Хотите ли вы ему доверять\? Murena сообщит в Google поддельную модель устройства, чтобы защитить вашу конфиденциальность. \nВы можете проверить , какое именное устройство, в \"Google\'s Device Activity\" после входа в систему. - ПРЕДУПРЕЖДЕНИЕ + ПРЕДУПРЕЖДЕНИЕ WebDAV Google Адресная книга Murena.io diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3217c8adfd2fd72dcc847e20ba04be37dc0b31e2..4b777fecf349a3a93c81b725aad44250c2cbc4e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,6 +9,8 @@ WebDAV Google e.foundation.webdav.google + Yahoo + e.foundation.webdav.yahoo Murena.io e.foundation.webdav.eelo foundation.e.accountmanager.address_book @@ -17,6 +19,8 @@ Murena.io Address book foundation.e.accountmanager.google.address_book Google Address book + foundation.e.accountmanager.yahoo.address_book + Yahoo Address book foundation.e.accountmanager.addressbooks Address books Remove @@ -537,9 +541,10 @@ Added account successfully - - WARNING + WARNING + Murena will report a fake device model to Google to protect your privacy.\nYou can check which one on Google\'s Device Activity after you log in. + Murena will report a fake device model to Yahoo to protect your privacy.\nYou can check which one on Yahoo\'s Device Activity after you log in. My Account Murena.io Account overview diff --git a/app/src/main/res/xml/account_authenticator_yahoo_address_book.xml b/app/src/main/res/xml/account_authenticator_yahoo_address_book.xml new file mode 100644 index 0000000000000000000000000000000000000000..f7e5a7a1a47ca0a640c968f27396e7754ab06085 --- /dev/null +++ b/app/src/main/res/xml/account_authenticator_yahoo_address_book.xml @@ -0,0 +1,15 @@ + + + diff --git a/app/src/main/res/xml/yahoo_account_authenticator.xml b/app/src/main/res/xml/yahoo_account_authenticator.xml new file mode 100644 index 0000000000000000000000000000000000000000..6d885759169bb2c0161d2463608722073c85df64 --- /dev/null +++ b/app/src/main/res/xml/yahoo_account_authenticator.xml @@ -0,0 +1,15 @@ + + + diff --git a/app/src/main/res/xml/yahoo_sync_address_books.xml b/app/src/main/res/xml/yahoo_sync_address_books.xml new file mode 100644 index 0000000000000000000000000000000000000000..abfd2f9d6a2abe586e1d8596ee23ba200a0ddb59 --- /dev/null +++ b/app/src/main/res/xml/yahoo_sync_address_books.xml @@ -0,0 +1,14 @@ + + + diff --git a/app/src/main/res/xml/yahoo_sync_calendars.xml b/app/src/main/res/xml/yahoo_sync_calendars.xml new file mode 100644 index 0000000000000000000000000000000000000000..7ec627d4be16ce0a15a8c8848c6ac9268c644ce2 --- /dev/null +++ b/app/src/main/res/xml/yahoo_sync_calendars.xml @@ -0,0 +1,16 @@ + + + diff --git a/app/src/main/res/xml/yahoo_sync_contacts.xml b/app/src/main/res/xml/yahoo_sync_contacts.xml new file mode 100644 index 0000000000000000000000000000000000000000..c933ec286be5b981bf3e8f463a1ec79eb36d1d4a --- /dev/null +++ b/app/src/main/res/xml/yahoo_sync_contacts.xml @@ -0,0 +1,15 @@ + + + diff --git a/app/src/main/res/xml/yahoo_sync_email.xml b/app/src/main/res/xml/yahoo_sync_email.xml new file mode 100644 index 0000000000000000000000000000000000000000..2f01fe490737b13d6bed4689f4137afa43193093 --- /dev/null +++ b/app/src/main/res/xml/yahoo_sync_email.xml @@ -0,0 +1,14 @@ + + + diff --git a/app/src/main/res/xml/yahoo_sync_tasks.xml b/app/src/main/res/xml/yahoo_sync_tasks.xml new file mode 100644 index 0000000000000000000000000000000000000000..5edba6fa0fd95d5f2a6f2e3508addb7fdafef064 --- /dev/null +++ b/app/src/main/res/xml/yahoo_sync_tasks.xml @@ -0,0 +1,15 @@ + + +