From bf14c2d91ec251433b58f1fe108e509d3e1a05b2 Mon Sep 17 00:00:00 2001 From: Narinder Rana Date: Mon, 1 Aug 2022 08:41:44 +0530 Subject: [PATCH 1/8] Created new account types for Google --- app/src/main/AndroidManifest.xml | 14 +++ .../GoogleAccountAuthenticatorService.kt | 114 ++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 99f49fac6..97c602a23 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -150,6 +150,20 @@ android:label="@string/webdav_add_mount_title" android:parentActivityName=".ui.webdav.WebdavMountsActivity" /> + + + + + + + + + + ?) { + thread { + cleanupAccounts(this) + } + } + + + private class AccountAuthenticator( + val context: Context + ) : AbstractAccountAuthenticator(context) { + + override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?): Bundle { + val intent = Intent(context, LoginActivity::class.java) + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) + 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 + + } +} -- GitLab From 78d8cb2e3d011533616c9ede815e7e0a26479234 Mon Sep 17 00:00:00 2001 From: Narinder Rana Date: Mon, 1 Aug 2022 16:44:18 +0530 Subject: [PATCH 2/8] Implement Google login --- app/build.gradle | 3 + app/src/main/AndroidManifest.xml | 42 ++ .../authorization/IdentityProvider.java | 256 +++++++++++ .../davdroid/settings/AccountSettings.kt | 5 + .../GoogleAccountAuthenticatorService.kt | 59 ++- .../ui/setup/GoogleAuthenticatorFragment.kt | 427 ++++++++++++++++++ .../ui/setup/GoogleAuthenticatorModel.kt | 49 ++ .../davdroid/ui/setup/LoginActivity.kt | 2 +- .../layout/fragment_google_authenticator.xml | 25 + .../values/account_providers_auth_config.xml | 18 + app/src/main/res/values/strings.xml | 9 +- app/src/main/res/values/styles.xml | 22 + gradle.properties | 1 + gradle/wrapper/gradle-wrapper.properties | 5 +- 14 files changed, 910 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/at/bitfire/davdroid/authorization/IdentityProvider.java create mode 100644 app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt create mode 100644 app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorModel.kt create mode 100644 app/src/main/res/layout/fragment_google_authenticator.xml create mode 100644 app/src/main/res/values/account_providers_auth_config.xml diff --git a/app/build.gradle b/app/build.gradle index dace8855f..417df1af9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,6 +12,7 @@ android { compileSdkVersion 32 buildToolsVersion '32.0.0' + defaultConfig { applicationId "at.bitfire.davdroid" @@ -152,6 +153,8 @@ dependencies { //noinspection GradleDependency implementation "org.apache.commons:commons-text:${versions.commonsText}" + implementation 'net.openid:appauth:0.7.0' + // for tests androidTestImplementation "com.google.dagger:hilt-android-testing:${versions.hilt}" kaptAndroidTest "com.google.dagger:hilt-android-compiler:${versions.hilt}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 97c602a23..4fe931f75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -163,6 +163,48 @@ android:resource="@xml/google_account_authenticator" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + PROVIDERS = Arrays.asList( + GOOGLE); + + public static List getEnabledProviders(Context context) + { + ArrayList providers = new ArrayList<>(); + for (IdentityProvider provider : PROVIDERS) + { + provider.readConfiguration(context); + providers.add(provider); + } + return providers; + } + + @NonNull + public final String name; + + @StringRes + public final int buttonContentDescriptionRes; + + @StringRes + private final int mDiscoveryEndpointRes; + + @StringRes + private final int mAuthEndpointRes; + + @StringRes + private final int mTokenEndpointRes; + + @StringRes + private final int mClientIdRes; + + @StringRes + private final int mClientSecretRes; + + @StringRes + private final int mRedirectUriRes; + + @StringRes + private final int mScopeRes; + + private boolean mConfigurationRead = false; + private Uri mDiscoveryEndpoint; + private Uri mAuthEndpoint; + private Uri mTokenEndpoint; + private String mClientId; + private String mClientSecret; + private Uri mRedirectUri; + private String mScope; + + IdentityProvider( + @NonNull String name, + @StringRes int discoveryEndpointRes, + @StringRes int authEndpointRes, + @StringRes int tokenEndpointRes, + @StringRes int clientIdRes, + @StringRes int clientSecretRes, + @StringRes int redirectUriRes, + @StringRes int scopeRes, + @StringRes int buttonContentDescriptionRes) + { + if (!isSpecified(discoveryEndpointRes) + && !isSpecified(authEndpointRes) + && !isSpecified(tokenEndpointRes)) + { + throw new IllegalArgumentException( + "the discovery endpoint or the auth and token endpoints must be specified"); + } + + this.name = name; + this.mDiscoveryEndpointRes = discoveryEndpointRes; + this.mAuthEndpointRes = authEndpointRes; + this.mTokenEndpointRes = tokenEndpointRes; + this.mClientIdRes = checkSpecified(clientIdRes, "clientIdRes"); + this.mClientSecretRes = clientSecretRes; + this.mRedirectUriRes = checkSpecified(redirectUriRes, "redirectUriRes"); + this.mScopeRes = checkSpecified(scopeRes, "scopeRes"); + this.buttonContentDescriptionRes = + checkSpecified(buttonContentDescriptionRes, "buttonContentDescriptionRes"); + } + + /** + * This must be called before any of the getters will function. + */ + public void readConfiguration(Context context) + { + if (mConfigurationRead) + { + return; + } + + Resources res = context.getResources(); + + mDiscoveryEndpoint = isSpecified(mDiscoveryEndpointRes) + ? getUriResource(res, mDiscoveryEndpointRes, "discoveryEndpointRes") + : null; + mAuthEndpoint = isSpecified(mAuthEndpointRes) + ? getUriResource(res, mAuthEndpointRes, "authEndpointRes") + : null; + mTokenEndpoint = isSpecified(mTokenEndpointRes) + ? getUriResource(res, mTokenEndpointRes, "tokenEndpointRes") + : null; + mClientId = res.getString(mClientIdRes); + mClientSecret = isSpecified(mClientSecretRes) ? res.getString(mClientSecretRes) : null; + mRedirectUri = getUriResource(res, mRedirectUriRes, "mRedirectUriRes"); + mScope = res.getString(mScopeRes); + + mConfigurationRead = true; + } + + private void checkConfigurationRead() + { + if (!mConfigurationRead) + { + throw new IllegalStateException("Configuration not read"); + } + } + + @Nullable + public Uri getDiscoveryEndpoint() + { + checkConfigurationRead(); + return mDiscoveryEndpoint; + } + + @Nullable + public Uri getAuthEndpoint() + { + checkConfigurationRead(); + return mAuthEndpoint; + } + + @Nullable + public Uri getTokenEndpoint() + { + checkConfigurationRead(); + return mTokenEndpoint; + } + + @NonNull + public String getClientId() + { + checkConfigurationRead(); + return mClientId; + } + + @Nullable + public String getClientSecret() + { + checkConfigurationRead(); + return mClientSecret; + } + + @NonNull + public Uri getRedirectUri() + { + checkConfigurationRead(); + return mRedirectUri; + } + + @NonNull + public String getScope() + { + checkConfigurationRead(); + return mScope; + } + + public void retrieveConfig(Context context, + RetrieveConfigurationCallback callback) + { + readConfiguration(context); + if (getDiscoveryEndpoint() != null) + { + AuthorizationServiceConfiguration.fetchFromUrl(mDiscoveryEndpoint, callback); + } + else + { + AuthorizationServiceConfiguration config = + new AuthorizationServiceConfiguration(mAuthEndpoint, mTokenEndpoint, null); + callback.onFetchConfigurationCompleted(config, null); + } + } + + private static boolean isSpecified(int value) + { + return value != NOT_SPECIFIED; + } + + private static int checkSpecified(int value, String valueName) + { + if (value == NOT_SPECIFIED) + { + throw new IllegalArgumentException(valueName + " must be specified"); + } + return value; + } + + private static Uri getUriResource(Resources res, @StringRes int resId, String resName) + { + return Uri.parse(res.getString(resId)); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt index 8d414c121..cfa3d757c 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt @@ -89,6 +89,9 @@ class AccountSettings( const val KEY_SYNC_INTERVAL_TASKS = "sync_interval_tasks" const val KEY_USERNAME = "user_name" + const val KEY_EMAIL_ADDRESS = "email_address" + const val KEY_AUTH_STATE = "auth_state" + const val KEY_CERTIFICATE_ALIAS = "certificate_alias" const val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false) @@ -139,10 +142,12 @@ class AccountSettings( bundle.putString(KEY_SETTINGS_VERSION, CURRENT_VERSION.toString()) if (credentials != null) { + bundle.putString(KEY_EMAIL_ADDRESS, credentials.userName) if (credentials.userName != null) bundle.putString(KEY_USERNAME, credentials.userName) if (credentials.certificateAlias != null) bundle.putString(KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) + } return bundle 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 2593bb540..57165ec43 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt @@ -13,15 +13,15 @@ import android.accounts.* import android.app.Service import android.content.Context import android.content.Intent -import android.database.DatabaseUtils import android.os.Bundle -import androidx.annotation.WorkerThread import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.ui.setup.LoginActivity -import foundation.e.accountmanager.R -import java.util.* +import at.bitfire.davdroid.R +import at.bitfire.davdroid.settings.AccountSettings +import net.openid.appauth.AuthState +import net.openid.appauth.AuthorizationService import java.util.logging.Level import kotlin.concurrent.thread @@ -39,12 +39,21 @@ class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener { Logger.log.info("Cleaning up orphaned accounts") val accountManager = AccountManager.get(context) - val accountNames = accountManager.getAccountsByType(context.getString(R.string.account_type)) - .map { it.name } + + val accountNames = HashSet() + val accounts = ArrayList() + accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)).forEach { accounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.google_account_type)).forEach { accounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { accounts.add(it) } + for (account in accounts.toTypedArray()) + accountNames += account.name // delete orphaned address book accounts - accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)) - .map { LocalAddressBook(context, it, null) } + val addressBookAccounts = ArrayList() + accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book)).forEach { addressBookAccounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book)).forEach { addressBookAccounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).forEach { addressBookAccounts.add(it) } + addressBookAccounts.map { LocalAddressBook(context, it, null) } .forEach { try { if (!accountNames.contains(it.mainAccount.name)) @@ -98,6 +107,7 @@ class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener { override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?): Bundle { val intent = Intent(context, LoginActivity::class.java) intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) + intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_GOOGLE) val bundle = Bundle(1) bundle.putParcelable(AccountManager.KEY_INTENT, intent) return bundle @@ -107,8 +117,39 @@ class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener { 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 getAuthToken(response: AccountAuthenticatorResponse?, account: Account?, authTokenType: String?, options: Bundle?): Bundle { + val accountManager = AccountManager.get(context) + val authState = AuthState.jsonDeserialize(accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE)) + + if (authState != null) { + if (authState.needsTokenRefresh) { + val tokenRequest = authState.createTokenRefreshRequest() + + AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex -> + authState.update(tokenResponse, ex) + accountManager.setUserData(account, AccountSettings.KEY_AUTH_STATE, authState.jsonSerializeString()) + val result = Bundle() + result.putString(AccountManager.KEY_ACCOUNT_NAME, account!!.name) + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) + result.putString(AccountManager.KEY_AUTHTOKEN, authState.accessToken) + response?.onResult(result) + } + } + else { + val result = Bundle() + result.putString(AccountManager.KEY_ACCOUNT_NAME, account!!.name) + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) + result.putString(AccountManager.KEY_AUTHTOKEN, authState.accessToken) + return result + } + } + + val result = Bundle() + result.putInt(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION) + return result + } override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?) = null } } + diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt new file mode 100644 index 000000000..5b78e8f51 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt @@ -0,0 +1,427 @@ +package at.bitfire.davdroid.ui.setup + +import android.app.Activity +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.* +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup + +import at.bitfire.davdroid.R +import androidx.lifecycle.ViewModelProviders +import net.openid.appauth.* +import org.json.JSONException +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.net.* +import org.json.JSONObject +import java.util.HashMap + +import android.net.ConnectivityManager +import android.text.Layout +import android.text.SpannableString +import android.text.style.AlignmentSpan +import android.widget.Toast +import at.bitfire.davdroid.authorization.IdentityProvider +import at.bitfire.davdroid.databinding.FragmentGoogleAuthenticatorBinding +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +import at.bitfire.davdroid.db.Credentials + +class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenResponseCallback { + + private lateinit var model: GoogleAuthenticatorModel + private lateinit var loginModel: LoginModel + + private val extraAuthServiceDiscovery = "authServiceDiscovery" + private val extraClientSecret = "clientSecret" + + private var authState: AuthState? = null + private var authorizationService: AuthorizationService? = null + + private val bufferSize = 1024 + private var userInfoJson: JSONObject? = null + + private fun isNetworkAvailable(): Boolean { + val connectivityManager = activity!!.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetworkInfo = connectivityManager.activeNetworkInfo + return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + + model = ViewModelProviders.of(this).get(GoogleAuthenticatorModel::class.java) + loginModel = ViewModelProviders.of(requireActivity()).get(LoginModel::class.java) + + // Initialise the authorization service + authorizationService = AuthorizationService(context!!) + + + + val v = FragmentGoogleAuthenticatorBinding.inflate(inflater, container, false) + v.lifecycleOwner = this + v.model = model + + + + activity?.intent?.let { + model.initialize(it) + val builder = MaterialAlertDialogBuilder(context!!, R.style.CustomAlertDialogStyle) + + if (!with(it) { getBooleanExtra(LoginActivity.ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE, false) }) { + val title = SpannableString(getString(R.string.google_alert_title)) + // alert dialog title align center + title.setSpan( + AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), + 0, + title.length, + 0 + ) + + builder.setTitle(title) + builder.setMessage(getString(R.string.google_alert_message)) + builder.setPositiveButton(android.R.string.yes) { dialog, which -> + // Get all the account providers + val providers = IdentityProvider.getEnabledProviders(context) + + // Iterate over the account providers + for (idp in providers) { + val retrieveCallback = AuthorizationServiceConfiguration.RetrieveConfigurationCallback { serviceConfiguration, ex -> + if (ex == null && serviceConfiguration != null) { + makeAuthRequest(serviceConfiguration, idp) + } else if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } else { + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + activity!!.finish() + } + } + + if (idp.name == getString(R.string.google_name)) { + // Get configurations for the Google account provider + idp.retrieveConfig(context, retrieveCallback) + } + } + } + builder.setCancelable(false) + + val dialog = builder.create() + dialog.show() + + } + else { + if (authState == null) { + val response = AuthorizationResponse.fromIntent(activity!!.intent) + val ex = AuthorizationException.fromIntent(activity!!.intent) + authState = AuthState(response, ex) + + if (response != null) { + exchangeAuthorizationCode(response) + } else if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } else { + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + activity!!.finish() + } + } + } + } + + return v.root + } + + private fun makeAuthRequest( + serviceConfig: AuthorizationServiceConfiguration, + idp: IdentityProvider) { + + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } + + val authRequest = AuthorizationRequest.Builder( + serviceConfig, + idp.clientId, + ResponseTypeValues.CODE, + idp.redirectUri) + .setScope(idp.scope) + .build() + + authorizationService?.performAuthorizationRequest( + authRequest, + createPostAuthorizationIntent( + context!!, + authRequest, + serviceConfig.discoveryDoc, + idp.clientSecret), + authorizationService?.createCustomTabsIntentBuilder()!! + .build()) + + requireActivity().setResult(Activity.RESULT_OK) + requireActivity().finish() + } + + private fun createPostAuthorizationIntent( + context: Context, + request: AuthorizationRequest, + discoveryDoc: AuthorizationServiceDiscovery?, + clientSecret: String?): PendingIntent { + val intent = Intent(context, LoginActivity::class.java) + + if (discoveryDoc != null) { + intent.putExtra(extraAuthServiceDiscovery, discoveryDoc.docJson.toString()) + } + + if (clientSecret != null) { + intent.putExtra(extraClientSecret, clientSecret) + } + + intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_GOOGLE) + intent.putExtra(LoginActivity.ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE, true) + + return PendingIntent.getActivity(context, request.hashCode(), intent, 0) + } + + private fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) { + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } + + val additionalParams = HashMap() + if (getClientSecretFromIntent(activity!!.intent) != null) { + additionalParams["client_secret"] = getClientSecretFromIntent(activity!!.intent) + } + performTokenRequest(authorizationResponse.createTokenExchangeRequest(additionalParams)) + } + + private fun getClientSecretFromIntent(intent: Intent): String? { + return if (!intent.hasExtra(extraClientSecret)) { + null + } + else intent.getStringExtra(extraClientSecret) + } + + + private fun performTokenRequest(request: TokenRequest) { + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } + + authorizationService?.performTokenRequest( + request, this) + } + + override fun onTokenRequestCompleted(response: TokenResponse?, ex: AuthorizationException?) { + authState?.update(response, ex) + + getAccountInfo() + } + + private fun getAccountInfo() { + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } + + val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent) + + if (!authState!!.isAuthorized + || discoveryDoc == null + || discoveryDoc.userinfoEndpoint == null) { + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + activity!!.finish() + } + else { + object : AsyncTask() { + override fun doInBackground(vararg params: Void): Void? { + if (fetchUserInfo()) { + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + activity!!.finish() + } + + return null + } + }.execute() + } + } + + private fun getDiscoveryDocFromIntent(intent: Intent): AuthorizationServiceDiscovery? { + if (!intent.hasExtra(extraAuthServiceDiscovery)) { + return null + } + val discoveryJson = intent.getStringExtra(extraAuthServiceDiscovery) + try { + return AuthorizationServiceDiscovery(JSONObject(discoveryJson)) + } + catch (ex: JSONException) { + throw IllegalStateException("Malformed JSON in discovery doc") + } + catch (ex: AuthorizationServiceDiscovery.MissingArgumentException) { + throw IllegalStateException("Malformed JSON in discovery doc") + } + + } + + private fun fetchUserInfo(): Boolean { + var error = false + + if (authState!!.authorizationServiceConfiguration == null) { + return true + } + + authState!!.performActionWithFreshTokens(authorizationService!!, AuthState.AuthStateAction { accessToken, _, ex -> + if (ex != null) { + error = true + return@AuthStateAction + } + + val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent) + ?: throw IllegalStateException("no available discovery doc") + + val userInfoEndpoint: URL + try { + userInfoEndpoint = URL(discoveryDoc.userinfoEndpoint!!.toString()) + } + catch (urlEx: MalformedURLException) { + error = true + return@AuthStateAction + } + + var userInfoResponse: InputStream? = null + try { + val conn = userInfoEndpoint.openConnection() as HttpURLConnection + conn.setRequestProperty("Authorization", "Bearer " + accessToken!!) + conn.instanceFollowRedirects = false + userInfoResponse = conn.inputStream + val response = readStream(userInfoResponse) + updateUserInfo(JSONObject(response)) + } + catch (ioEx: IOException) { + error = true + } + catch (jsonEx: JSONException) { + error = true + } + finally { + if (userInfoResponse != null) { + try { + userInfoResponse.close() + } + catch (ioEx: IOException) { + error = true + } + + } + } + }) + + return error + } + + @Throws(IOException::class) + private fun readStream(stream: InputStream?): String { + val br = BufferedReader(InputStreamReader(stream!!)) + val buffer = CharArray(bufferSize) + val sb = StringBuilder() + var readCount = br.read(buffer) + while (readCount != -1) { + sb.append(buffer, 0, readCount) + readCount = br.read(buffer) + } + return sb.toString() + } + + private fun updateUserInfo(jsonObject: JSONObject) { + Handler(Looper.getMainLooper()).post { + userInfoJson = jsonObject + onAccountInfoGotten() + } + } + + private fun onAccountInfoGotten() { + if (!isNetworkAvailable()) { + Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show() + activity!!.finish() + } + + if (userInfoJson != null) { + try { + + var emailAddress = "" + if (userInfoJson!!.has("email")) { + emailAddress = userInfoJson!!.getString("email") + } + + if (validate(emailAddress, authState!!)) + requireFragmentManager().beginTransaction() + .replace(android.R.id.content, DetectConfigurationFragment(), null) + .addToBackStack(null) + .commit() + + } + catch (ex: JSONException) { + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + activity!!.finish() + } + + } + else { + Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show() + activity!!.finish() + } + + } + + private fun validate(emailAddress: String, authState: AuthState): Boolean { + var valid = false + + fun validateUrl() { + model.baseUrlError.value = null + try { + val uri = URI("https://apidata.googleusercontent.com/caldav/v2/$emailAddress/user") + if (uri.scheme.equals("http", true) || uri.scheme.equals("https", true)) { + valid = true + loginModel.baseURI = uri + } else + model.baseUrlError.value = getString(R.string.login_url_must_be_http_or_https) + } catch (e: Exception) { + model.baseUrlError.value = e.localizedMessage + } + } + + when { + + model.loginWithUrlAndTokens.value == true -> { + validateUrl() + + model.usernameError.value = null + + if (loginModel.baseURI != null) { + valid = true + loginModel.credentials = Credentials(emailAddress, null) + } + } + + } + + return valid + } + + override fun onDestroy() { + super.onDestroy() + authorizationService?.dispose() + } + + +} + diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorModel.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorModel.kt new file mode 100644 index 000000000..6adf20ff6 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorModel.kt @@ -0,0 +1,49 @@ +package at.bitfire.davdroid.ui.setup + +import android.content.Intent +import android.net.Uri +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class GoogleAuthenticatorModel: ViewModel() { + + private var initialized = false + + val loginWithUrlAndTokens = MutableLiveData() + + val baseUrl = MutableLiveData() + val baseUrlError = MutableLiveData() + + val emailAddress = MutableLiveData() + val emailAddressError = MutableLiveData() + + val username = MutableLiveData() + val usernameError = MutableLiveData() + + val password = MutableLiveData() + val passwordError = MutableLiveData() + + val certificateAlias = MutableLiveData() + val certificateAliasError = MutableLiveData() + + init { + loginWithUrlAndTokens.value = true + } + + fun initialize(intent: Intent) { + if (initialized) + return + + // we've got initial login data + val givenUrl = intent.getStringExtra(LoginActivity.EXTRA_URL) + val givenUsername = intent.getStringExtra(LoginActivity.EXTRA_USERNAME) + val givenPassword = intent.getStringExtra(LoginActivity.EXTRA_PASSWORD) + + baseUrl.value = givenUrl + + password.value = givenPassword + + initialized = true + } + +} 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 801f82729..dbb58955b 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 @@ -47,7 +47,7 @@ class LoginActivity: AppCompatActivity() { const val SETUP_ACCOUNT_PROVIDER_TYPE = "setup_account_provider_type" const val ACCOUNT_PROVIDER_EELO = "eelo" const val ACCOUNT_PROVIDER_GOOGLE = "google" - + const val ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE = "google_auth_complete" } @Inject diff --git a/app/src/main/res/layout/fragment_google_authenticator.xml b/app/src/main/res/layout/fragment_google_authenticator.xml new file mode 100644 index 000000000..988020999 --- /dev/null +++ b/app/src/main/res/layout/fragment_google_authenticator.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/account_providers_auth_config.xml b/app/src/main/res/values/account_providers_auth_config.xml new file mode 100644 index 000000000..b3b10fdba --- /dev/null +++ b/app/src/main/res/values/account_providers_auth_config.xml @@ -0,0 +1,18 @@ + + + + + Google + + 100496780587-pbiu5eudcjm6cge2phduc6mt8mgbsmsr.apps.googleusercontent.com + + https://accounts.google.com/.well-known/openid-configuration + + openid profile email https://www.googleapis.com/auth/carddav https://www.googleapis.com/auth/calendar https://mail.google.com/ + + + com.googleusercontent.apps.100496780587-pbiu5eudcjm6cge2phduc6mt8mgbsmsr:/oauth2redirect + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eebfb386f..cdcbc91b1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,9 +8,16 @@ Account does not exist (anymore) bitfire.at.davdroid - Google + Google /e/ bitfire.at.davdroid.google + e.foundation.webdav.eelo + + + WARNING + /e/ 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. + + at.bitfire.davdroid.address_book /e/ diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 75ffbab89..5deef5934 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -23,6 +23,7 @@ #aee571 #4b830d @android:color/black + @color/primaryTextColor #ff6d00 #ff9e40 @@ -86,4 +87,25 @@ @color/secondaryLightColor + + + + + + diff --git a/gradle.properties b/gradle.properties index c6bf06227..52bbc6160 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,3 +16,4 @@ android.useAndroidX=true org.gradle.daemon=true org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1536M" org.gradle.parallel=true +android.enableJetifier=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 92f06b50f..02b1c085d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Fri Jul 29 11:01:22 IST 2022 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME -- GitLab From 3bd9df98e76c7539bd62b2e7b1f4232652997e46 Mon Sep 17 00:00:00 2001 From: Narinder Rana Date: Tue, 2 Aug 2022 13:40:03 +0530 Subject: [PATCH 3/8] add and link to ACCOUNT_PROVIDER_GOOGLE --- .../at/bitfire/davdroid/db/Credentials.kt | 40 +++++++++++++--- .../davdroid/settings/AccountSettings.kt | 46 +++++++++++++++---- .../davdroid/ui/account/SettingsActivity.kt | 6 +-- .../setup/DefaultLoginCredentialsFragment.kt | 2 +- .../ui/setup/GoogleAuthenticatorFragment.kt | 6 +-- .../davdroid/ui/setup/LoginActivity.kt | 6 ++- .../davdroid/webdav/CredentialsStore.kt | 1 + 7 files changed, 82 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt b/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt index f2f4758b1..9b43df98c 100644 --- a/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt +++ b/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt @@ -4,15 +4,41 @@ package at.bitfire.davdroid.db +import net.openid.appauth.AuthState +import java.net.URI + + data class Credentials( - val userName: String? = null, - val password: String? = null, - val certificateAlias: String? = null + val userName: String? = null, + val password: String? = null, + val authState: AuthState? = null, + val certificateAlias: String? = null, + val serverUri: URI? = null ) { - override fun toString(): String { - val maskedPassword = "*****".takeIf { password != null } - return "Credentials(userName=$userName, password=$maskedPassword, certificateAlias=$certificateAlias)" + enum class Type { + UsernamePassword, + OAuth, + ClientCertificate } -} \ No newline at end of file + val type: Type + + init { + type = when { + !certificateAlias.isNullOrEmpty() -> + Type.ClientCertificate + !userName.isNullOrEmpty() && (authState != null) -> + Type.OAuth + !userName.isNullOrEmpty() && !password.isNullOrEmpty() -> + Type.UsernamePassword + else -> + throw IllegalArgumentException("Invalid account type/credentials") + } + } + + override fun toString() = + "Credentials(type=$type, userName=$userName, certificateAlias=$certificateAlias)" + +} + diff --git a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt index cfa3d757c..babbb0844 100644 --- a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt @@ -47,6 +47,7 @@ import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.property.Url +import net.openid.appauth.AuthState import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.apache.commons.lang3.StringUtils import org.dmfs.tasks.contract.TaskContract @@ -234,16 +235,45 @@ class AccountSettings( // authentication settings - fun credentials() = Credentials( - accountManager.getUserData(account, KEY_USERNAME), - accountManager.getPassword(account), - accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS) - ) +// fun credentials() = Credentials( +// +//// accountManager.getUserData(account, KEY_USERNAME), +//// accountManager.getPassword(account), +//// null, +//// accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS) +// ) + + fun credentials(): Credentials { + if (accountManager.getUserData(account, KEY_AUTH_STATE).isNullOrEmpty()) { + return Credentials( + accountManager.getUserData(account, KEY_USERNAME), + accountManager.getPassword(account), + null, + accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS)) + } + else { + return Credentials( + accountManager.getUserData(account, KEY_USERNAME), + accountManager.getPassword(account), + AuthState.jsonDeserialize(accountManager.getUserData(account, KEY_AUTH_STATE)), + accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS)) + } + } + fun credentials(credentials: Credentials) { - accountManager.setUserData(account, KEY_USERNAME, credentials.userName) - accountManager.setPassword(account, credentials.password) - accountManager.setUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) + if (credentials.authState == null) { + accountManager.setUserData(account, KEY_USERNAME, credentials.userName) + accountManager.setPassword(account, credentials.password) + accountManager.setUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) + } + else { + accountManager.setUserData(account, KEY_USERNAME, credentials.userName) + accountManager.setPassword(account, credentials.password) + accountManager.setUserData(account, KEY_AUTH_STATE, credentials.authState.jsonSerializeString()) + accountManager.setUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) + } + } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/SettingsActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/SettingsActivity.kt index b4a58e02f..3407a6616 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/account/SettingsActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/account/SettingsActivity.kt @@ -226,14 +226,14 @@ class SettingsActivity: AppCompatActivity() { prefUserName.summary = credentials.userName prefUserName.text = credentials.userName prefUserName.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newUserName -> - model.updateCredentials(Credentials(newUserName as String, credentials.password, credentials.certificateAlias)) + model.updateCredentials(Credentials(newUserName as String, credentials.password)) false } if (credentials.userName != null) { prefPassword.isVisible = true prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newPassword -> - model.updateCredentials(Credentials(credentials.userName, newPassword as String, credentials.certificateAlias)) + model.updateCredentials(Credentials(credentials.userName, newPassword as String)) false } } else @@ -242,7 +242,7 @@ class SettingsActivity: AppCompatActivity() { prefCertAlias.summary = credentials.certificateAlias ?: getString(R.string.settings_certificate_alias_empty) prefCertAlias.setOnPreferenceClickListener { KeyChain.choosePrivateKeyAlias(requireActivity(), { newAlias -> - model.updateCredentials(Credentials(credentials.userName, credentials.password, newAlias)) + model.updateCredentials(Credentials(certificateAlias = newAlias)) }, null, null, null, -1, credentials.certificateAlias) true } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsFragment.kt index 445079025..ea7522aee 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/DefaultLoginCredentialsFragment.kt @@ -179,7 +179,7 @@ class DefaultLoginCredentialsFragment : Fragment() { loginModel.credentials = when { // username/password and client certificate model.loginUseUsernamePassword.value == true && model.loginUseClientCertificate.value == true -> - Credentials(username, password, alias) + Credentials(username, password, null) // user/name password only model.loginUseUsernamePassword.value == true && model.loginUseClientCertificate.value == false -> diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt index 5b78e8f51..1dde99099 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt @@ -62,14 +62,10 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon // Initialise the authorization service authorizationService = AuthorizationService(context!!) - - val v = FragmentGoogleAuthenticatorBinding.inflate(inflater, container, false) v.lifecycleOwner = this v.model = model - - activity?.intent?.let { model.initialize(it) val builder = MaterialAlertDialogBuilder(context!!, R.style.CustomAlertDialogStyle) @@ -408,7 +404,7 @@ class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenRespon if (loginModel.baseURI != null) { valid = true - loginModel.credentials = Credentials(emailAddress, null) + loginModel.credentials = Credentials(emailAddress, null, authState, null) } } 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 dbb58955b..fecaf076d 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 @@ -78,7 +78,11 @@ class LoginActivity: AppCompatActivity() { .commit() } ACCOUNT_PROVIDER_GOOGLE -> { - Log.i(TAG, "starting google account sign-in") + Log.e(TAG, "starting google account sign-in") + supportFragmentManager.beginTransaction() + .replace(android.R.id.content, GoogleAuthenticatorFragment()) + .commit() + } else -> // first call, add first login fragment diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/CredentialsStore.kt b/app/src/main/java/at/bitfire/davdroid/webdav/CredentialsStore.kt index 87ceb5aa0..647dc2d55 100644 --- a/app/src/main/java/at/bitfire/davdroid/webdav/CredentialsStore.kt +++ b/app/src/main/java/at/bitfire/davdroid/webdav/CredentialsStore.kt @@ -43,6 +43,7 @@ class CredentialsStore(context: Context) { return Credentials( prefs.getString(keyName(mountId, USER_NAME), null), prefs.getString(keyName(mountId, PASSWORD), null), + null, prefs.getString(keyName(mountId, CERTIFICATE_ALIAS), null) ) } -- GitLab From ad3d8ee03386f4a6bcddf927923a73affa8c43b3 Mon Sep 17 00:00:00 2001 From: Narinder Rana Date: Wed, 3 Aug 2022 15:25:22 +0530 Subject: [PATCH 4/8] Show eelo and Google calendar accounts in their own custom account types --- .../11.json | 39 +- app/src/main/AndroidManifest.xml | 66 ++++ .../java/at/bitfire/davdroid/Constants.kt | 9 + .../java/at/bitfire/davdroid/DavService.kt | 3 +- .../at/bitfire/davdroid/db/AppDatabase.kt | 4 +- .../java/at/bitfire/davdroid/db/HomeSet.kt | 27 +- .../java/at/bitfire/davdroid/db/Service.kt | 8 +- .../davdroid/resource/LocalTaskList.kt | 16 + .../AccountAuthenticatorService.kt | 80 +++- .../davdroid/ui/AccountListFragment.kt | 6 + .../ui/setup/AccountDetailsFragment.kt | 344 +++++++++++++++--- app/src/main/res/xml/eelo_sync_calendars.xml | 15 + app/src/main/res/xml/eelo_sync_tasks.xml | 14 + .../main/res/xml/google_sync_calendars.xml | 15 + app/src/main/res/xml/google_sync_tasks.xml | 14 + 15 files changed, 586 insertions(+), 74 deletions(-) create mode 100644 app/src/main/res/xml/eelo_sync_calendars.xml create mode 100644 app/src/main/res/xml/eelo_sync_tasks.xml create mode 100644 app/src/main/res/xml/google_sync_calendars.xml create mode 100644 app/src/main/res/xml/google_sync_tasks.xml diff --git a/app/schemas/at.bitfire.davdroid.db.AppDatabase/11.json b/app/schemas/at.bitfire.davdroid.db.AppDatabase/11.json index 77f44ee87..3c763a39b 100644 --- a/app/schemas/at.bitfire.davdroid.db.AppDatabase/11.json +++ b/app/schemas/at.bitfire.davdroid.db.AppDatabase/11.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 11, - "identityHash": "223aa7f0fd53730921ca212a663585d8", + "identityHash": "18ccf6722c2ce4e125ba28a8a99659fa", "entities": [ { "tableName": "service", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `authState` TEXT, `accountType` TEXT, `addressBookAccountType` TEXT, `type` TEXT NOT NULL, `principal` TEXT)", "fields": [ { "fieldPath": "id", @@ -20,6 +20,24 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "authState", + "columnName": "authState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountType", + "columnName": "accountType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addressBookAccountType", + "columnName": "addressBookAccountType", + "affinity": "TEXT", + "notNull": false + }, { "fieldPath": "type", "columnName": "type", @@ -47,6 +65,7 @@ "accountName", "type" ], + "orders": [], "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)" } ], @@ -54,7 +73,7 @@ }, { "tableName": "homeset", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -68,12 +87,6 @@ "affinity": "INTEGER", "notNull": true }, - { - "fieldPath": "personal", - "columnName": "personal", - "affinity": "INTEGER", - "notNull": true - }, { "fieldPath": "url", "columnName": "url", @@ -107,6 +120,7 @@ "serviceId", "url" ], + "orders": [], "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)" } ], @@ -251,6 +265,7 @@ "serviceId", "type" ], + "orders": [], "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)" }, { @@ -260,6 +275,7 @@ "homeSetId", "type" ], + "orders": [], "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)" }, { @@ -268,6 +284,7 @@ "columnNames": [ "url" ], + "orders": [], "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)" } ], @@ -339,6 +356,7 @@ "collectionId", "authority" ], + "orders": [], "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)" } ], @@ -466,6 +484,7 @@ "parentId", "name" ], + "orders": [], "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)" } ], @@ -530,7 +549,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '223aa7f0fd53730921ca212a663585d8')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '18ccf6722c2ce4e125ba28a8a99659fa')" ] } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4fe931f75..947ead762 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -163,6 +163,70 @@ android:resource="@xml/google_account_authenticator" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -227,6 +291,7 @@ + + = Build.VERSION_CODES.M) + return context.packageManager.resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null + else + try { + TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)?.use { + return true + } + } catch (e: Exception) { + // couldn't acquire task provider + } + return false + } + + fun create(account: Account, provider: TaskProvider, info: Collection): Uri { val values = valuesFromCollectionInfo(info, true) values.put(TaskLists.OWNER, account.name) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.kt index 8244e02f8..8c6443212 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.kt @@ -3,34 +3,98 @@ **************************************************************************************************/ package at.bitfire.davdroid.syncadapter -import android.accounts.AbstractAccountAuthenticator -import android.accounts.Account -import android.accounts.AccountAuthenticatorResponse -import android.accounts.AccountManager +import android.accounts.* import android.app.Service import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.annotation.WorkerThread +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.ui.setup.LoginActivity +import java.util.logging.Level +import kotlin.concurrent.thread + + /** * Account authenticator for the main DAVx5 account type. */ -class AccountAuthenticatorService: Service() { +class AccountAuthenticatorService: Service(), OnAccountsUpdateListener { + + companion object { + + @WorkerThread + fun cleanupAccounts(context: Context) { + Logger.log.info("Cleaning up orphaned accounts") + + val accountManager = AccountManager.get(context) + val accountNames = HashSet() + val accounts = ArrayList() + accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)).forEach { accounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.google_account_type)).forEach { accounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { accounts.add(it) } + for (account in accounts.toTypedArray()) + accountNames += account.name + + // delete orphaned address book accounts + val addressBookAccounts = ArrayList() + accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book)).forEach { addressBookAccounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book)).forEach { addressBookAccounts.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).forEach { addressBookAccounts.add(it) } + addressBookAccounts.map { LocalAddressBook(context, it, null) } + .forEach { + try { + if (!accountNames.contains(it.mainAccount.name)) + it.delete() + } catch(e: Exception) { + Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e) + } + } + + // delete orphaned services in DB + val db = AppDatabase.getInstance(context) + val serviceDao = db.serviceDao() + if (accountNames.isEmpty()) + serviceDao.deleteAll() + else + serviceDao.deleteExceptAccounts(accountNames.toTypedArray()) + } + + } + + private lateinit var accountManager: AccountManager private lateinit var accountAuthenticator: AccountAuthenticator override fun onCreate() { + accountManager = AccountManager.get(this) + accountManager.addOnAccountsUpdatedListener(this, null, true) + accountAuthenticator = AccountAuthenticator(this) } + override fun onDestroy() { + super.onDestroy() + accountManager.removeOnAccountsUpdatedListener(this) + } + override fun onBind(intent: Intent?) = - accountAuthenticator.iBinder.takeIf { intent?.action == AccountManager.ACTION_AUTHENTICATOR_INTENT } + accountAuthenticator.iBinder.takeIf { intent?.action == AccountManager.ACTION_AUTHENTICATOR_INTENT } + + + override fun onAccountsUpdated(accounts: Array?) { + thread { + cleanupAccounts(this) + } + } private class AccountAuthenticator( - val context: Context + val context: Context ): AbstractAccountAuthenticator(context) { override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?): Bundle { @@ -49,5 +113,5 @@ class AccountAuthenticatorService: Service() { override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?) = null } - } + diff --git a/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt index a1fe1f1ae..8098e490a 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt @@ -263,7 +263,13 @@ class AccountListFragment: Fragment() { val accountsWithInfo = sortedAccounts.map { account -> AccountInfo(account, DavUtils.accountSyncStatus(context, syncAuthorities, account)) } + val account = ArrayList() + val accountManager = AccountManager.get(context) + accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)).forEach { account.add(it) } + accountManager.getAccountsByType(context.getString(R.string.google_account_type)).forEach { account.add(it) } + accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { account.add(it) } accounts.postValue(accountsWithInfo) + } } 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 074aabd3d..21794a34f 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 @@ -6,6 +6,8 @@ package at.bitfire.davdroid.ui.setup import android.accounts.Account import android.accounts.AccountManager +import android.app.Activity +import android.app.Application import android.content.ContentResolver import android.content.Context import android.content.Intent @@ -20,6 +22,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.* +import at.bitfire.davdroid.Constants import at.bitfire.davdroid.DavService import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R @@ -29,12 +32,14 @@ import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.db.HomeSet import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.resource.LocalTaskList import at.bitfire.davdroid.resource.TaskUtils import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.ui.account.AccountActivity +import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint @@ -45,6 +50,7 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch import java.util.logging.Level import javax.inject.Inject +import kotlin.concurrent.thread @AndroidEntryPoint class AccountDetailsFragment : Fragment() { @@ -99,10 +105,11 @@ class AccountDetailsFragment : Fragment() { v.createAccount.visibility = View.GONE model.createAccount( - name, - loginModel.credentials, - config, - GroupMethod.valueOf(groupMethodName) + requireActivity(), + name, + loginModel.credentials!!, + config, + GroupMethod.valueOf(groupMethodName) ).observe(viewLifecycleOwner, Observer { success -> if (success) { // close Create account activity @@ -138,49 +145,249 @@ class AccountDetailsFragment : Fragment() { } +// @HiltViewModel +// class AccountDetailsModel @Inject constructor( +// @ApplicationContext val context: Context, +// val db: AppDatabase, +// val settingsManager: SettingsManager +// ) : ViewModel() { +// +// val name = MutableLiveData() +// val nameError = MutableLiveData() +// val showApostropheWarning = MutableLiveData(false) +// +// fun validateAccountName(s: Editable) { +// showApostropheWarning.value = s.toString().contains('\'') +// nameError.value = null +// } +// +// fun createAccount(name: String, credentials: Credentials?, config: DavResourceFinder.Configuration, groupMethod: GroupMethod): LiveData { +// val result = MutableLiveData() +// viewModelScope.launch(Dispatchers.Default + NonCancellable) { +// val account = Account(name, context.getString(R.string.account_type)) +// +// // create Android account +// val userData = AccountSettings.initialUserData(credentials) +// Logger.log.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, userData)) +// +// if (!AccountUtils.createAccount(context, account, userData, credentials?.password)) { +// result.postValue(false) +// return@launch +// } +// +// // add entries for account to service DB +// Logger.log.log(Level.INFO, "Writing account configuration to database", config) +// try { +// val accountSettings = AccountSettings(context, account) +// val defaultSyncInterval = settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL) +// +// val refreshIntent = Intent(context, DavService::class.java) +// refreshIntent.action = DavService.ACTION_REFRESH_COLLECTIONS +// +// val addrBookAuthority = context.getString(R.string.address_books_authority) +// if (config.cardDAV != null) { +// // insert CardDAV service +// val id = insertService(db, name, credentials.authState!!.jsonSerializeString(), accountType, Service.TYPE_CALDAV, config.calDAV) +// // initial CardDAV account settings +// accountSettings.setGroupMethod(groupMethod) +// +// // start CardDAV service detection (refresh collections) +// refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id) +// context.startService(refreshIntent) +// +// // set default sync interval and enable sync regardless of permissions +// ContentResolver.setIsSyncable(account, addrBookAuthority, 1) +// accountSettings.setSyncInterval(addrBookAuthority, defaultSyncInterval) +// } else +// ContentResolver.setIsSyncable(account, addrBookAuthority, 0) +// +// if (config.calDAV != null) { +// // insert CalDAV service +// val id = insertService(db, name, credentials.authState!!.jsonSerializeString(), accountType, Service.TYPE_CALDAV, config.calDAV) +// // start CalDAV service detection (refresh collections) +// refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id) +// context.startService(refreshIntent) +// +// // set default sync interval and enable sync regardless of permissions +// ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1) +// accountSettings.setSyncInterval(CalendarContract.AUTHORITY, defaultSyncInterval) +// +// val taskProvider = TaskUtils.currentProvider(context) +// if (taskProvider != null) { +// ContentResolver.setIsSyncable(account, taskProvider.authority, 1) +// accountSettings.setSyncInterval(taskProvider.authority, defaultSyncInterval) +// // further changes will be handled by TasksWatcher on app start or when tasks app is (un)installed +// } +// } else +// ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0) +// +// } catch(e: InvalidAccountException) { +// Logger.log.log(Level.SEVERE, "Couldn't access account settings", e) +// result.postValue(false) +// return@launch +// } +// result.postValue(true) +// } +// return result +// } +// +// +// private fun insertService(db: AppDatabase, accountName: String, authState: String, accountType: String, type: String, info: DavResourceFinder.Configuration.ServiceInfo): Long { +// // insert service +// val service = Service(0, accountName, authState, accountType, type, info.principal) +// val serviceId = db.serviceDao().insertOrReplace(service) +// +// // insert home sets +// val homeSetDao = db.homeSetDao() +// for (homeSet in info.homeSets) { +// homeSetDao.insertOrReplace(HomeSet(0, serviceId, true, homeSet)) +// } +// +// // insert collections +// val collectionDao = db.collectionDao() +// for (collection in info.collections.values) { +// collection.serviceId = serviceId +// collectionDao.insertOrReplace(collection) +// } +// +// return serviceId +// } +// +// } + @HiltViewModel - class AccountDetailsModel @Inject constructor( - @ApplicationContext val context: Context, - val db: AppDatabase, - val settingsManager: SettingsManager - ) : ViewModel() { + class AccountDetailsModel( + application: Application + ) : AndroidViewModel(application) { val name = MutableLiveData() val nameError = MutableLiveData() - val showApostropheWarning = MutableLiveData(false) - - fun validateAccountName(s: Editable) { - showApostropheWarning.value = s.toString().contains('\'') - nameError.value = null - } - fun createAccount(name: String, credentials: Credentials?, config: DavResourceFinder.Configuration, groupMethod: GroupMethod): LiveData { + fun createAccount( + activity: Activity, + name: String, + credentials: Credentials, + config: DavResourceFinder.Configuration, + groupMethod: GroupMethod + ): LiveData { val result = MutableLiveData() - viewModelScope.launch(Dispatchers.Default + NonCancellable) { - val account = Account(name, context.getString(R.string.account_type)) + val context = getApplication() + thread { + var accountType = context!!.getString(R.string.account_type) + var addressBookAccountType = context!!.getString(R.string.account_type_address_book) + + var baseURL: String? = null + if (config.calDAV != null) { + baseURL = config.calDAV.principal.toString() + } + + when (activity!!.intent.getStringExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE)) { + LoginActivity.ACCOUNT_PROVIDER_EELO -> { + accountType = context!!.getString(R.string.eelo_account_type) + addressBookAccountType = + context!!.getString(R.string.account_type_eelo_address_book) + baseURL = credentials.serverUri.toString() + } + LoginActivity.ACCOUNT_PROVIDER_GOOGLE -> { + accountType = context!!.getString(R.string.google_account_type) + addressBookAccountType = + context!!.getString(R.string.account_type_google_address_book) + baseURL = null + } + } + + val account = Account(credentials.userName, accountType) // create Android account val userData = AccountSettings.initialUserData(credentials) - Logger.log.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, userData)) + Logger.log.log( + Level.INFO, + "Creating Android account with initial config", + arrayOf(account, userData) + ) + + val accountManager = AccountManager.get(context) + + if (!accountManager.addAccountExplicitly(account, credentials.password, userData)) { + if (accountType == context.getString(R.string.google_account_type)) { + for (googleAccount in accountManager.getAccountsByType( + context.getString( + R.string.google_account_type + ) + )) { + if (userData.get(AccountSettings.KEY_EMAIL_ADDRESS) == accountManager + .getUserData(account, AccountSettings.KEY_EMAIL_ADDRESS) + ) { + accountManager.setUserData( + googleAccount, AccountSettings.KEY_AUTH_STATE, + userData.getString(AccountSettings.KEY_AUTH_STATE) + ) + } + } + } else { + return@thread + } + } - if (!AccountUtils.createAccount(context, account, userData, credentials?.password)) { - result.postValue(false) - return@launch + if (!credentials.authState?.accessToken.isNullOrEmpty()) { + accountManager.setAuthToken( + account, + Constants.AUTH_TOKEN_TYPE, + credentials.authState!!.accessToken + ) + } + + if (!credentials.password.isNullOrEmpty()) { + accountManager.setPassword(account, credentials.password) } + ContentResolver.setSyncAutomatically( + account, + context.getString(R.string.notes_authority), + true + ) + ContentResolver.setSyncAutomatically( + account, + context.getString(R.string.email_authority), + true + ) + ContentResolver.setSyncAutomatically( + account, + context.getString(R.string.media_authority), + true + ) + ContentResolver.setSyncAutomatically( + account, + context.getString(R.string.app_data_authority), + true + ) + ContentResolver.setSyncAutomatically( + account, + context.getString(R.string.metered_edrive_authority), + true + ) + // add entries for account to service DB Logger.log.log(Level.INFO, "Writing account configuration to database", config) + val db = AppDatabase.getInstance(context) try { val accountSettings = AccountSettings(context, account) - val defaultSyncInterval = settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL) val refreshIntent = Intent(context, DavService::class.java) refreshIntent.action = DavService.ACTION_REFRESH_COLLECTIONS - val addrBookAuthority = context.getString(R.string.address_books_authority) if (config.cardDAV != null) { // insert CardDAV service - val id = insertService(name, Service.TYPE_CARDDAV, config.cardDAV) + + val id = insertService( + db, + credentials.userName!!, + credentials.authState?.jsonSerializeString(), + accountType, + addressBookAccountType, + Service.TYPE_CARDDAV, + config.cardDAV + ) // initial CardDAV account settings accountSettings.setGroupMethod(groupMethod) @@ -189,52 +396,97 @@ class AccountDetailsFragment : Fragment() { refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id) context.startService(refreshIntent) - // set default sync interval and enable sync regardless of permissions - ContentResolver.setIsSyncable(account, addrBookAuthority, 1) - accountSettings.setSyncInterval(addrBookAuthority, defaultSyncInterval) + // contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_address_books.xml + accountSettings.setSyncInterval( + context.getString(R.string.address_books_authority), + Constants.DEFAULT_CALENDAR_SYNC_INTERVAL + ) } else - ContentResolver.setIsSyncable(account, addrBookAuthority, 0) + ContentResolver.setIsSyncable( + account, + context.getString(R.string.address_books_authority), + 0 + ) if (config.calDAV != null) { // insert CalDAV service - val id = insertService(name, Service.TYPE_CALDAV, config.calDAV) + val id = insertService( + db, + credentials.userName!!, + credentials.authState?.jsonSerializeString(), + accountType, + addressBookAccountType, + Service.TYPE_CALDAV, + config.calDAV + ) // start CalDAV service detection (refresh collections) refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id) context.startService(refreshIntent) - // set default sync interval and enable sync regardless of permissions - ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1) - accountSettings.setSyncInterval(CalendarContract.AUTHORITY, defaultSyncInterval) - - val taskProvider = TaskUtils.currentProvider(context) - if (taskProvider != null) { - ContentResolver.setIsSyncable(account, taskProvider.authority, 1) - accountSettings.setSyncInterval(taskProvider.authority, defaultSyncInterval) - // further changes will be handled by TasksWatcher on app start or when tasks app is (un)installed + // calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_calendars.xml + accountSettings.setSyncInterval( + CalendarContract.AUTHORITY, + Constants.DEFAULT_CALENDAR_SYNC_INTERVAL + ) + + // enable task sync if OpenTasks is installed + // further changes will be handled by PackageChangedReceiver + if (LocalTaskList.tasksProviderAvailable(context)) { + ContentResolver.setIsSyncable( + account, + TaskProvider.ProviderName.OpenTasks.authority, + 1 + ) + accountSettings.setSyncInterval( + TaskProvider.ProviderName.OpenTasks.authority, + Constants.DEFAULT_CALENDAR_SYNC_INTERVAL + ) } - } else + } else { ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0) + ContentResolver.setIsSyncable( + account, + TaskProvider.ProviderName.OpenTasks.authority, + 0 + ) + } - } catch(e: InvalidAccountException) { + } catch (e: InvalidAccountException) { Logger.log.log(Level.SEVERE, "Couldn't access account settings", e) result.postValue(false) - return@launch + return@thread } result.postValue(true) } return result } - private fun insertService(accountName: String, type: String, info: DavResourceFinder.Configuration.ServiceInfo): Long { + private fun insertService( + db: AppDatabase, + accountName: String, + authState: String?, + accountType: String, + addressBookAccountType: String, + type: String, + info: DavResourceFinder.Configuration.ServiceInfo + ): Long { // insert service - val service = Service(0, accountName, type, info.principal) + val service = Service( + 0, + accountName, + authState, + accountType, + addressBookAccountType, + type, + info.principal + ) val serviceId = db.serviceDao().insertOrReplace(service) // insert home sets val homeSetDao = db.homeSetDao() for (homeSet in info.homeSets) { - homeSetDao.insertOrReplace(HomeSet(0, serviceId, true, homeSet)) + homeSetDao.insertOrReplace(HomeSet(0, serviceId, homeSet)) } // insert collections @@ -249,4 +501,6 @@ class AccountDetailsFragment : Fragment() { } + + } \ No newline at end of file diff --git a/app/src/main/res/xml/eelo_sync_calendars.xml b/app/src/main/res/xml/eelo_sync_calendars.xml new file mode 100644 index 000000000..701412cb7 --- /dev/null +++ b/app/src/main/res/xml/eelo_sync_calendars.xml @@ -0,0 +1,15 @@ + + + diff --git a/app/src/main/res/xml/eelo_sync_tasks.xml b/app/src/main/res/xml/eelo_sync_tasks.xml new file mode 100644 index 000000000..abd37480f --- /dev/null +++ b/app/src/main/res/xml/eelo_sync_tasks.xml @@ -0,0 +1,14 @@ + + + diff --git a/app/src/main/res/xml/google_sync_calendars.xml b/app/src/main/res/xml/google_sync_calendars.xml new file mode 100644 index 000000000..73572a6f7 --- /dev/null +++ b/app/src/main/res/xml/google_sync_calendars.xml @@ -0,0 +1,15 @@ + + + diff --git a/app/src/main/res/xml/google_sync_tasks.xml b/app/src/main/res/xml/google_sync_tasks.xml new file mode 100644 index 000000000..aae0677af --- /dev/null +++ b/app/src/main/res/xml/google_sync_tasks.xml @@ -0,0 +1,14 @@ + + + -- GitLab From d227759de31c7e3423e080a0fa19b022bb3a2620 Mon Sep 17 00:00:00 2001 From: Narinder Rana Date: Wed, 3 Aug 2022 16:24:03 +0530 Subject: [PATCH 5/8] update Login detail xml and styles for build --- .../ui/setup/AccountDetailsFragment.kt | 6 +- .../main/res/layout/login_account_details.xml | 74 +++++++++---------- app/src/main/res/values/styles.xml | 29 ++++++++ 3 files changed, 64 insertions(+), 45 deletions(-) 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 21794a34f..67d835db1 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 @@ -80,11 +80,7 @@ class AccountDetailsFragment : Fragment() { if (settings.containsKey(AccountSettings.KEY_CONTACT_GROUP_METHOD)) v.contactGroupMethod.isEnabled = false - // CalDAV-specific - config.calDAV?.let { - val accountNameAdapter = ArrayAdapter(requireActivity(), android.R.layout.simple_list_item_1, it.emails) - v.accountName.setAdapter(accountNameAdapter) - } + v.contactGroupMethod.setSelection(1) v.createAccount.setOnClickListener { val name = model.name.value diff --git a/app/src/main/res/layout/login_account_details.xml b/app/src/main/res/layout/login_account_details.xml index 4bfffaa34..21710a8e1 100644 --- a/app/src/main/res/layout/login_account_details.xml +++ b/app/src/main/res/layout/login_account_details.xml @@ -1,10 +1,17 @@ + + - @@ -36,37 +43,24 @@ - - + + android:textColor="@color/primaryTextColor" + android:text="@={details.name}" + app:error="@{details.nameError}" + android:inputType="textEmailAddress"/> - - + android:text="@string/login_account_name_info"/> - - -