diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8189e7a08da23c30f970e5928a3d8d2c29c13779..13e5107c30fd604d86088bed536b2f4e34ec893d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,31 +1,22 @@
-image: registry.gitlab.com/bitfireat/docker-android-emulator:latest
+image: "registry.gitlab.e.foundation:5000/e/apps/docker-android-apps-cicd:latest"
+
+stages:
+- build
before_script:
- - git submodule update --init --recursive
- - export GRADLE_USER_HOME=`pwd`/.gradle; chmod +x gradlew
+- git submodule update --init --recursive
+- export GRADLE_USER_HOME=$(pwd)/.gradle
+- chmod +x ./gradlew
cache:
+ key: ${CI_PROJECT_ID}
paths:
- - .gradle/
-
-test:
- tags:
- - privileged
- script:
- - start-emulator.sh
- - ./gradlew app:check app:connectedCheck
- artifacts:
- paths:
- - app/build/outputs/lint-results-debug.html
- - app/build/reports
- - build/reports
+ - .gradle/
-pages:
+build:
+ stage: build
script:
- - ./gradlew app:dokka
- - mkdir public && mv app/build/dokka public
+ - ./gradlew build
artifacts:
paths:
- - public
- only:
- - master-ose
+ - app/build/outputs/apk/standard/debug/app-standard-debug.apk
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000000000000000000000000000000000000..63358f26f2a666fe7832afe847c36a1fa539d1cd
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,3 @@
+© 2018-2019 - Author: Nihar Thakkar
+© 2018-2019 - Author: Vincent Bourgmayer
+© 2018-2019 - Author: Romain Hunault
diff --git a/README.md b/README.md
index 2a68c139c62c8475b20252d9c600261e3da6abf1..76aa1cd900272fdbc69fc26f7fe5721acd4079c8 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-
-
-DAVx⁵
+Account Manager
========
+Account Manager is a fork of DAVx⁵.
+
Please see the [DAVx⁵ Web site](https://www.davx5.com) for
comprehensive information about DAVx⁵.
diff --git a/app/build.gradle b/app/build.gradle
index c231dc793c57a69a03a0cf0434d89f0eb8704d04..d464b442b569324ae0b509e271b98b2e7c54fa99 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -72,6 +72,9 @@ android {
}
defaultConfig {
+ manifestPlaceholders = [
+ 'appAuthRedirectScheme': 'net.openid.appauthdemo'
+ ]
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -120,6 +123,7 @@ dependencies {
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
implementation 'com.google.android:flexbox:1.1.0'
implementation 'com.google.android.material:material:1.2.0-alpha03'
+ implementation 'androidx.browser:browser:1.0.0'
def room_version = '2.2.2'
implementation "androidx.room:room-runtime:$room_version"
@@ -138,6 +142,8 @@ dependencies {
implementation 'dnsjava:dnsjava:2.1.9'
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'org.apache.commons:commons-lang3:3.9'
+ implementation 'net.openid:appauth:0.7.0'
+ implementation 'com.google.android.material:material:1.0.0'
// for tests
androidTestImplementation 'androidx.test:runner:1.2.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cb08aa26022fdfbec150341fdf8f6ad016a9e8a3..1fe4e95adea71b9f54f60a7fa01582c9e92ebf1b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,7 +1,7 @@
@@ -53,7 +53,7 @@
android:theme="@style/AppTheme.NoActionBar">
-
+
@@ -75,7 +75,8 @@
@@ -194,6 +195,267 @@
android:resource="@xml/contacts"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png
index 4cc81409080c7754124235d4d31c11ce7a8434f0..cafde5df9d1e89e1bdc0c9800cacbdabf7bd084a 100644
Binary files a/app/src/main/ic_launcher-web.png and b/app/src/main/ic_launcher-web.png differ
diff --git a/app/src/main/java/foundation/e/accountmanager/Constants.kt b/app/src/main/java/foundation/e/accountmanager/Constants.kt
index 2d12577b284629dfe9bf2ab594e8e6cf3320b813..a2caf777c6f275b126673f3a1805434116bab47c 100644
--- a/app/src/main/java/foundation/e/accountmanager/Constants.kt
+++ b/app/src/main/java/foundation/e/accountmanager/Constants.kt
@@ -7,11 +7,17 @@
*/
package foundation.e.accountmanager
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
object Constants {
const val DAVDROID_GREEN_RGBA = 0xFF8bc34a.toInt()
- const val DEFAULT_SYNC_INTERVAL = 4 * 3600L // 4 hours
+ // NOTE: Android 7 and up don't allow 2 min sync frequencies unless system frameworks are modified
+ const val DEFAULT_CALENDAR_SYNC_INTERVAL = 2 * 60L // 2 minutes
+ const val DEFAULT_CONTACTS_SYNC_INTERVAL = 15 * 60L // 15 minutes
/**
* Context label for [org.apache.commons.lang3.exception.ContextedException].
@@ -27,4 +33,8 @@ object Constants {
*/
const val EXCEPTION_CONTEXT_REMOTE_RESOURCE = "remoteResource"
+ const val AUTH_TOKEN_TYPE = "oauth2-access-token"
+
+ const val EELO_SYNC_URL = "https://ecloud.global"
+
}
diff --git a/app/src/main/java/foundation/e/accountmanager/DavService.kt b/app/src/main/java/foundation/e/accountmanager/DavService.kt
index 4a4960299e1d98a0028caddafda6ff33f6fe7d0b..9b3cfbad0d7188b765082b392e14c868397f992b 100644
--- a/app/src/main/java/foundation/e/accountmanager/DavService.kt
+++ b/app/src/main/java/foundation/e/accountmanager/DavService.kt
@@ -28,6 +28,7 @@ import foundation.e.accountmanager.model.Collection
import foundation.e.accountmanager.settings.AccountSettings
import foundation.e.accountmanager.ui.DebugInfoActivity
import foundation.e.accountmanager.ui.NotificationUtils
+import net.openid.appauth.AuthState
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import java.lang.ref.WeakReference
@@ -35,6 +36,10 @@ import java.util.*
import java.util.logging.Level
import kotlin.concurrent.thread
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
class DavService: android.app.Service() {
companion object {
@@ -142,7 +147,7 @@ class DavService: android.app.Service() {
val collectionDao = db.collectionDao()
val service = db.serviceDao().get(serviceId) ?: throw IllegalArgumentException("Service not found")
- val account = Account(service.accountName, getString(R.string.account_type))
+ val account = Account(service.accountName, service.accountType)
val homeSets = homeSetDao.getByService(serviceId).associateBy { it.url }.toMutableMap()
val collections = collectionDao.getByService(serviceId).associateBy { it.url }.toMutableMap()
@@ -272,76 +277,78 @@ class DavService: android.app.Service() {
.build().use { client ->
val httpClient = client.okHttpClient
+ val accessToken = service.authState
// refresh home set list (from principal)
- service.principal?.let { principalUrl ->
- Logger.log.fine("Querying principal $principalUrl for home sets")
- queryHomeSets(httpClient, principalUrl)
- }
-
- // now refresh homesets and their member collections
- val itHomeSets = homeSets.iterator()
- while (itHomeSets.hasNext()) {
- val homeSet = itHomeSets.next()
- Logger.log.fine("Listing home set ${homeSet.key}")
-
- try {
- DavResource(httpClient, homeSet.key).propfind(1, *DAV_COLLECTION_PROPERTIES) { response, relation ->
- if (!response.isSuccess())
- return@propfind
-
- if (relation == Response.HrefRelation.SELF) {
- // this response is about the homeset itself
- homeSet.value.displayName = response[DisplayName::class.java]?.displayName
- homeSet.value.privBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind ?: true
- }
-
- // in any case, check whether the response is about a useable collection
- val info = Collection.fromDavResponse(response) ?: return@propfind
- info.serviceId = serviceId
- info.confirmed = true
- Logger.log.log(Level.FINE, "Found collection", info)
-
- // remember usable collections
- if ((service.type == Service.TYPE_CARDDAV && info.type == Collection.TYPE_ADDRESSBOOK) ||
- (service.type == Service.TYPE_CALDAV && arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(info.type)))
- collections[response.href] = info
- }
- } catch(e: HttpException) {
- if (e.code in arrayOf(403, 404, 410))
- // delete home set only if it was not accessible (40x)
- itHomeSets.remove()
+ service.principal?.let { principalUrl ->
+ Logger.log.fine("Querying principal $principalUrl for home sets")
+ queryHomeSets(httpClient, principalUrl)
}
- }
- // check/refresh unconfirmed collections
- val itCollections = collections.entries.iterator()
- while (itCollections.hasNext()) {
- val (url, info) = itCollections.next()
- if (!info.confirmed)
+ // now refresh homesets and their member collections
+ val itHomeSets = homeSets.iterator()
+ while (itHomeSets.hasNext()) {
+ val homeSet = itHomeSets.next()
+ Logger.log.fine("Listing home set ${homeSet.key}")
+
try {
- DavResource(httpClient, url).propfind(0, *DAV_COLLECTION_PROPERTIES) { response, _ ->
+ DavResource(httpClient, homeSet.key, accessToken).propfind(1, *DAV_COLLECTION_PROPERTIES) { response, relation ->
if (!response.isSuccess())
return@propfind
- val collection = Collection.fromDavResponse(response) ?: return@propfind
- collection.confirmed = true
-
- // remove unusable collections
- if ((service.type == Service.TYPE_CARDDAV && collection.type != Collection.TYPE_ADDRESSBOOK) ||
- (service.type == Service.TYPE_CALDAV && !arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(collection.type)) ||
- (collection.type == Collection.TYPE_WEBCAL && collection.source == null))
- itCollections.remove()
+ if (relation == Response.HrefRelation.SELF) {
+ // this response is about the homeset itself
+ homeSet.value.displayName = response[DisplayName::class.java]?.displayName
+ homeSet.value.privBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind ?: true
+ }
+
+ // in any case, check whether the response is about a useable collection
+ val info = Collection.fromDavResponse(response) ?: return@propfind
+ info.serviceId = serviceId
+ info.confirmed = true
+ Logger.log.log(Level.FINE, "Found collection", info)
+
+ // remember usable collections
+ if ((service.type == Service.TYPE_CARDDAV && info.type == Collection.TYPE_ADDRESSBOOK) ||
+ (service.type == Service.TYPE_CALDAV && arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(info.type)))
+ collections[response.href] = info
}
} catch(e: HttpException) {
if (e.code in arrayOf(403, 404, 410))
- // delete collection only if it was not accessible (40x)
- itCollections.remove()
- else
- throw e
+ // delete home set only if it was not accessible (40x)
+ itHomeSets.remove()
}
- }
- }
+ }
+
+ // check/refresh unconfirmed collections
+ val itCollections = collections.entries.iterator()
+ while (itCollections.hasNext()) {
+ val (url, info) = itCollections.next()
+ if (!info.confirmed)
+ try {
+ DavResource(httpClient, url, accessToken).propfind(0, *DAV_COLLECTION_PROPERTIES) { response, _ ->
+ if (!response.isSuccess())
+ return@propfind
+
+ val collection = Collection.fromDavResponse(response) ?: return@propfind
+ collection.confirmed = true
+
+ // remove unusable collections
+ if ((service.type == Service.TYPE_CARDDAV && collection.type != Collection.TYPE_ADDRESSBOOK) ||
+ (service.type == Service.TYPE_CALDAV && !arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(collection.type)) ||
+ (collection.type == Collection.TYPE_WEBCAL && collection.source == null))
+ itCollections.remove()
+ }
+ } catch(e: HttpException) {
+ if (e.code in arrayOf(403, 404, 410))
+ // delete collection only if it was not accessible (40x)
+ itCollections.remove()
+ else
+ throw e
+ }
+ }
+ }
+
saveResults()
} catch(e: InvalidAccountException) {
@@ -372,4 +379,4 @@ class DavService: android.app.Service() {
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/PackageChangedReceiver.kt b/app/src/main/java/foundation/e/accountmanager/PackageChangedReceiver.kt
index 808557cccd4405dbe07ff5bf9bdc0a1303a2df07..3f6f4aca3954fac3a1b7887bb4eb6f3b839f2a68 100644
--- a/app/src/main/java/foundation/e/accountmanager/PackageChangedReceiver.kt
+++ b/app/src/main/java/foundation/e/accountmanager/PackageChangedReceiver.kt
@@ -22,6 +22,10 @@ import foundation.e.accountmanager.resource.LocalTaskList
import foundation.e.ical4android.TaskProvider.ProviderName.OpenTasks
import kotlin.concurrent.thread
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
class PackageChangedReceiver: BroadcastReceiver() {
companion object {
@@ -34,11 +38,11 @@ class PackageChangedReceiver: BroadcastReceiver() {
// check all accounts and (de)activate OpenTasks if a CalDAV service is defined
val db = AppDatabase.getInstance(context)
db.serviceDao().getByType(Service.TYPE_CALDAV).forEach { service ->
- val account = Account(service.accountName, context.getString(R.string.account_type))
+ val account = Account(service.accountName, service.accountType)
if (tasksInstalled) {
if (ContentResolver.getIsSyncable(account, OpenTasks.authority) <= 0) {
ContentResolver.setIsSyncable(account, OpenTasks.authority, 1)
- ContentResolver.addPeriodicSync(account, OpenTasks.authority, Bundle(), Constants.DEFAULT_SYNC_INTERVAL)
+ ContentResolver.addPeriodicSync(account, OpenTasks.authority, Bundle(), Constants.DEFAULT_CALENDAR_SYNC_INTERVAL)
}
} else
ContentResolver.setIsSyncable(account, OpenTasks.authority, 0)
diff --git a/app/src/main/java/foundation/e/accountmanager/authorization/IdentityProvider.java b/app/src/main/java/foundation/e/accountmanager/authorization/IdentityProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..543c1e0771b11ec5320874b259b6eea113a88538
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/authorization/IdentityProvider.java
@@ -0,0 +1,259 @@
+package foundation.e.accountmanager.authorization;
+
+/*
+ * Copyright 2015 The AppAuth Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+import foundation.e.accountmanager.R;
+
+import net.openid.appauth.AuthorizationServiceConfiguration;
+import net.openid.appauth.AuthorizationServiceConfiguration.RetrieveConfigurationCallback;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Authors: Nihar Thakkar and others
+ *
+ * An abstraction of identity providers, containing all necessary info for the demo app.
+ */
+public class IdentityProvider
+{
+
+ /**
+ * Value used to indicate that a configured property is not specified or required.
+ */
+ public static final int NOT_SPECIFIED = -1;
+
+ public static final IdentityProvider GOOGLE = new IdentityProvider(
+ "Google",
+ R.string.google_discovery_uri,
+ NOT_SPECIFIED, // auth endpoint is discovered
+ NOT_SPECIFIED, // token endpoint is discovered
+ R.string.google_client_id,
+ NOT_SPECIFIED, // client secret is not required for Google
+ R.string.google_auth_redirect_uri,
+ R.string.google_scope_string,
+ R.string.google_name);
+
+ public static final List 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/foundation/e/accountmanager/model/AppDatabase.kt b/app/src/main/java/foundation/e/accountmanager/model/AppDatabase.kt
index 3a1b84669326101700aa8eee445ae15a42b9bd45..a8078a470f2fa7e8a21bfaf84f3f02067d0ab44c 100644
--- a/app/src/main/java/foundation/e/accountmanager/model/AppDatabase.kt
+++ b/app/src/main/java/foundation/e/accountmanager/model/AppDatabase.kt
@@ -11,6 +11,10 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import foundation.e.accountmanager.log.Logger
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
@Suppress("ClassName")
@Database(entities = [
Service::class,
@@ -114,11 +118,14 @@ abstract class AppDatabase: RoomDatabase() {
"CREATE TABLE service(" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"accountName TEXT NOT NULL," +
+ "authState TEXT," +
+ "accountType TEXT," +
+ "addressBookAccountType TEXT," +
"type TEXT NOT NULL," +
"principal TEXT DEFAULT NULL" +
")",
"CREATE UNIQUE INDEX index_service_accountName_type ON service(accountName, type)",
- "INSERT INTO service(id, accountName, type, principal) SELECT _id, accountName, service, principal FROM services",
+ "INSERT INTO service(id, accountName, authState, accountType, addressBookAccountType, type, principal) SELECT _id, accountName, authState, accountType, addressBookAccountType, service, principal FROM services",
"DROP TABLE services",
// migrate "homesets" to "homeset": rename columns, make id NOT NULL
@@ -221,4 +228,4 @@ abstract class AppDatabase: RoomDatabase() {
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/model/Collection.kt b/app/src/main/java/foundation/e/accountmanager/model/Collection.kt
index 43146738caea9de9b593405f0952d4f977f8b742..ede34873c3f67c744ebaf213b6cad034eac66035 100644
--- a/app/src/main/java/foundation/e/accountmanager/model/Collection.kt
+++ b/app/src/main/java/foundation/e/accountmanager/model/Collection.kt
@@ -50,7 +50,7 @@ data class Collection(
var source: HttpUrl? = null,
/** whether this collection has been selected for synchronization */
- var sync: Boolean = false
+ var sync: Boolean = true
): IdEntity() {
@@ -154,4 +154,4 @@ data class Collection(
fun title() = displayName ?: DavUtils.lastSegmentOfUrl(url)
fun readOnly() = forceReadOnly || !privWriteContent
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/model/Credentials.kt b/app/src/main/java/foundation/e/accountmanager/model/Credentials.kt
index c85d5155e581732d04658e733587cd3de4ca88a5..6196a535bbb2ac1c0d4549125201e1ff7a6fd0c5 100644
--- a/app/src/main/java/foundation/e/accountmanager/model/Credentials.kt
+++ b/app/src/main/java/foundation/e/accountmanager/model/Credentials.kt
@@ -8,14 +8,24 @@
package foundation.e.accountmanager.model
+import net.openid.appauth.AuthState
+import java.net.URI
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
class Credentials(
val userName: String? = null,
val password: String? = null,
- val certificateAlias: String? = null
+ val authState: AuthState? = null,
+ val certificateAlias: String? = null,
+ val serverUri: URI? = null
) {
enum class Type {
UsernamePassword,
+ OAuth,
ClientCertificate
}
@@ -25,14 +35,16 @@ class Credentials(
type = when {
!certificateAlias.isNullOrEmpty() ->
Type.ClientCertificate
+ !userName.isNullOrEmpty() && (authState != null) ->
+ Type.OAuth
!userName.isNullOrEmpty() && !password.isNullOrEmpty() ->
Type.UsernamePassword
else ->
- throw IllegalArgumentException("Either username/password or certificate alias must be set")
+ throw IllegalArgumentException("Invalid account type/credentials")
}
}
override fun toString() =
"Credentials(type=$type, userName=$userName, certificateAlias=$certificateAlias)"
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/model/Service.kt b/app/src/main/java/foundation/e/accountmanager/model/Service.kt
index 7b2a2c93824827d560c5d6bb4f92412039aa807c..acc37ba7da7e8d939e4a185822e52b913f170170 100644
--- a/app/src/main/java/foundation/e/accountmanager/model/Service.kt
+++ b/app/src/main/java/foundation/e/accountmanager/model/Service.kt
@@ -17,6 +17,10 @@ data class Service(
var accountName: String,
var type: String,
+ var authState: String,
+ var accountType: String,
+
+ var addressBookAccountType: String,
var principal: HttpUrl?
) {
@@ -25,4 +29,4 @@ data class Service(
const val TYPE_CARDDAV = "carddav"
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/model/ServiceDao.kt b/app/src/main/java/foundation/e/accountmanager/model/ServiceDao.kt
index e595692b784d586d0df5c589bf29e44e26231de6..55e82cd930a5ccfc6d3e3e32266e6936731c56cb 100644
--- a/app/src/main/java/foundation/e/accountmanager/model/ServiceDao.kt
+++ b/app/src/main/java/foundation/e/accountmanager/model/ServiceDao.kt
@@ -20,6 +20,9 @@ interface ServiceDao {
@Query("SELECT * FROM service WHERE type=:type")
fun getByType(type: String): List
+ @Query("SELECT * FROM service WHERE accountName=:accountName")
+ fun getByAccountName(accountName: String): Service?
+
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(service: Service): Long
@@ -32,4 +35,4 @@ interface ServiceDao {
@Query("UPDATE service SET accountName=:newName WHERE accountName=:oldName")
fun renameAccount(oldName: String, newName: String)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/resource/LocalAddressBook.kt b/app/src/main/java/foundation/e/accountmanager/resource/LocalAddressBook.kt
index dc9ddba8d9bde23e2288e538e31eb99792ee3d04..8d726f65e97feaba2c259107730a818880ae828e 100644
--- a/app/src/main/java/foundation/e/accountmanager/resource/LocalAddressBook.kt
+++ b/app/src/main/java/foundation/e/accountmanager/resource/LocalAddressBook.kt
@@ -22,6 +22,7 @@ import foundation.e.accountmanager.DavUtils
import foundation.e.accountmanager.R
import foundation.e.accountmanager.log.Logger
import foundation.e.accountmanager.model.Collection
+import foundation.e.accountmanager.model.AppDatabase
import foundation.e.accountmanager.model.SyncState
import foundation.e.vcard4android.*
import java.io.ByteArrayOutputStream
@@ -50,7 +51,11 @@ class LocalAddressBook(
fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, info: Collection): LocalAddressBook {
val accountManager = AccountManager.get(context)
- val account = Account(accountName(mainAccount, info), context.getString(R.string.account_type_address_book))
+ var account = Account(accountName(mainAccount, info), context.getString(R.string.account_type_address_book))
+
+ val db = AppDatabase.getInstance(context)
+ val service = db.serviceDao().getByAccountName(mainAccount.name) ?: throw IllegalArgumentException("Service not found")
+ account = Account(accountName(mainAccount, info), service.addressBookAccountType)
val userData = initialUserData(mainAccount, info.url.toString())
Logger.log.log(Level.INFO, "Creating local address book $account", userData)
if (!accountManager.addAccountExplicitly(account, null, userData))
@@ -76,11 +81,17 @@ class LocalAddressBook(
return addressBook
}
- fun findAll(context: Context, provider: ContentProviderClient?, mainAccount: Account?) = AccountManager.get(context)
- .getAccountsByType(context.getString(R.string.account_type_address_book))
- .map { LocalAddressBook(context, it, provider) }
+ fun findAll(context: Context, provider: ContentProviderClient?, mainAccount: Account?): List {
+ val accountManager = AccountManager.get(context)
+ val accounts = ArrayList()
+ accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book)).forEach { accounts.add(it) }
+ accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book)).forEach { accounts.add(it) }
+ accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).forEach { accounts.add(it) }
+
+ return accounts.toTypedArray().map { LocalAddressBook(context, it, provider) }
.filter { mainAccount == null || it.mainAccount == mainAccount }
.toList()
+ }
fun accountName(mainAccount: Account, info: Collection): String {
val baos = ByteArrayOutputStream()
@@ -106,7 +117,10 @@ class LocalAddressBook(
}
fun mainAccount(context: Context, account: Account): Account =
- if (account.type == context.getString(R.string.account_type_address_book)) {
+ if (account.type == context.getString(R.string.account_type_address_book) ||
+ account.type == context.getString(R.string.account_type_eelo_address_book) ||
+ account.type == context.getString(R.string.account_type_google_address_book)
+ ) {
val manager = AccountManager.get(context)
Account(
manager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME),
@@ -374,4 +388,4 @@ class LocalAddressBook(
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/settings/AccountSettings.kt b/app/src/main/java/foundation/e/accountmanager/settings/AccountSettings.kt
index b4440926ce26f341177dc5bbd719b276711b0fb1..f5575ba828b581c9434615fdd0c097af2013cef0 100644
--- a/app/src/main/java/foundation/e/accountmanager/settings/AccountSettings.kt
+++ b/app/src/main/java/foundation/e/accountmanager/settings/AccountSettings.kt
@@ -34,12 +34,15 @@ import foundation.e.ical4android.TaskProvider
import foundation.e.ical4android.TaskProvider.ProviderName.OpenTasks
import foundation.e.vcard4android.ContactsStorageException
import foundation.e.vcard4android.GroupMethod
+import net.openid.appauth.AuthState
import okhttp3.HttpUrl
import org.apache.commons.lang3.StringUtils
import org.dmfs.tasks.contract.TaskContract
import java.util.logging.Level
/**
+ * Authors: Nihar Thakkar and others
+ *
* Manages settings of an account.
*
* @throws InvalidAccountException on construction when the account doesn't exist (anymore)
@@ -55,6 +58,8 @@ class AccountSettings(
const val KEY_SETTINGS_VERSION = "version"
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)
@@ -95,16 +100,27 @@ class AccountSettings(
const val SYNC_INTERVAL_MANUALLY = -1L
- fun initialUserData(credentials: Credentials): Bundle {
+ fun initialUserData(credentials: Credentials, baseURL: String?): Bundle {
val bundle = Bundle(2)
bundle.putString(KEY_SETTINGS_VERSION, CURRENT_VERSION.toString())
when (credentials.type) {
- Credentials.Type.UsernamePassword ->
+ Credentials.Type.UsernamePassword -> {
+ bundle.putString(KEY_USERNAME, credentials.userName)
+ bundle.putString(KEY_EMAIL_ADDRESS, credentials.userName)
+ }
+ Credentials.Type.OAuth -> {
bundle.putString(KEY_USERNAME, credentials.userName)
+ bundle.putString(KEY_EMAIL_ADDRESS, credentials.userName)
+ bundle.putString(KEY_AUTH_STATE, credentials.authState!!.jsonSerializeString())
+ }
Credentials.Type.ClientCertificate ->
bundle.putString(KEY_CERTIFICATE_ALIAS, credentials.certificateAlias)
}
+
+ if (!baseURL.isNullOrEmpty()) {
+ bundle.putString("oc_base_url", baseURL)
+ }
return bundle
}
@@ -133,16 +149,35 @@ class AccountSettings(
// authentication settings
- fun credentials() = Credentials(
- accountManager.getUserData(account, KEY_USERNAME),
- accountManager.getPassword(account),
- 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)
+ }
}
@@ -427,7 +462,7 @@ class AccountSettings(
// request sync of new address book account
ContentResolver.setIsSyncable(account, context.getString(R.string.address_books_authority), 1)
- setSyncInterval(context.getString(R.string.address_books_authority), Constants.DEFAULT_SYNC_INTERVAL)
+ setSyncInterval(context.getString(R.string.address_books_authority), Constants.DEFAULT_CONTACTS_SYNC_INTERVAL)
}
/* Android 7.1.1 OpenTasks fix */
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/AccountAuthenticatorService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/AccountAuthenticatorService.kt
index 29ec996e096c74e1aba276e68cc962e0494f0946..9ba80923e9b62b9f7b0a229d8d8cc5764c560d9b 100644
--- a/app/src/main/java/foundation/e/accountmanager/syncadapter/AccountAuthenticatorService.kt
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/AccountAuthenticatorService.kt
@@ -37,12 +37,20 @@ class AccountAuthenticatorService: 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))
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/CalendarSyncManager.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/CalendarSyncManager.kt
index 0e547ab0211c50b779007e1dfbc09d4a45fa935b..00f66a49c515c381c8f4ef3c21529761fdd826be 100644
--- a/app/src/main/java/foundation/e/accountmanager/syncadapter/CalendarSyncManager.kt
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/CalendarSyncManager.kt
@@ -39,6 +39,8 @@ import java.util.*
import java.util.logging.Level
/**
+ * Authors: Nihar Thakkar and others
+ *
* Synchronization manager for CalDAV collections; handles events (VEVENT)
*/
class CalendarSyncManager(
@@ -53,7 +55,7 @@ class CalendarSyncManager(
override fun prepare(): Boolean {
collectionURL = HttpUrl.parse(localCollection.name ?: return false) ?: return false
- davCollection = DavCalendar(httpClient.okHttpClient, collectionURL)
+ davCollection = DavCalendar(httpClient.okHttpClient, collectionURL, accountSettings.credentials().authState?.accessToken)
// if there are dirty exceptions for events, mark their master events as dirty, too
localCollection.processDirtyExceptions()
@@ -116,7 +118,7 @@ class CalendarSyncManager(
if (bunch.size == 1) {
val remote = bunch.first()
// only one contact, use GET
- useRemote(DavResource(httpClient.okHttpClient, remote)) { resource ->
+ useRemote(DavResource(httpClient.okHttpClient, remote, accountSettings.credentials().authState?.accessToken)) { resource ->
resource.get(DavCalendar.MIME_ICALENDAR.toString()) { response ->
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
val eTag = response.header("ETag")?.let { GetETag(it).eTag }
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/CalendarsSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/CalendarsSyncAdapterService.kt
index e6ba170204c479698090a02d95dfadc859198eda..b4985871431707940c0979407456ba7be64aaf0a 100644
--- a/app/src/main/java/foundation/e/accountmanager/syncadapter/CalendarsSyncAdapterService.kt
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/CalendarsSyncAdapterService.kt
@@ -13,17 +13,24 @@ import android.content.ContentResolver
import android.content.Context
import android.content.SyncResult
import android.os.Bundle
+import android.os.AsyncTask
import android.provider.CalendarContract
import foundation.e.accountmanager.log.Logger
import foundation.e.accountmanager.model.AppDatabase
import foundation.e.accountmanager.model.Collection
+import foundation.e.accountmanager.model.Credentials
import foundation.e.accountmanager.model.Service
import foundation.e.accountmanager.resource.LocalCalendar
import foundation.e.accountmanager.settings.AccountSettings
import foundation.e.ical4android.AndroidCalendar
+import net.openid.appauth.AuthorizationService
import okhttp3.HttpUrl
import java.util.logging.Level
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
class CalendarsSyncAdapterService: SyncAdapterService() {
override fun syncAdapter() = CalendarsSyncAdapter(this)
@@ -58,7 +65,33 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
for (calendar in calendars) {
Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
CalendarSyncManager(context, account, accountSettings, extras, authority, syncResult, calendar).use {
- it.performSync()
+ val authState = accountSettings.credentials().authState
+ if (authState != null)
+ {
+ if (authState.needsTokenRefresh)
+ {
+ val tokenRequest = authState.createTokenRefreshRequest()
+
+ AuthorizationService(context).performTokenRequest(tokenRequest,
+ AuthorizationService.TokenResponseCallback { tokenResponse, ex ->
+ authState.update(tokenResponse, ex)
+ accountSettings.credentials(Credentials(account.name, null, authState, null))
+ it.accountSettings.credentials(Credentials(it.account.name, null, authState, null))
+ object : AsyncTask() {
+ override fun doInBackground(vararg params: Void): Void? {
+ it.performSync()
+ return null
+ }
+ }.execute()
+ })
+ }
+ else {
+ it.performSync()
+ }
+ }
+ else {
+ it.performSync()
+ }
}
}
} catch(e: Exception) {
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/ContactsSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/ContactsSyncAdapterService.kt
index 5df902d333581f04e90bd9483e7a28e0667dd605..1aab6d06e14a5d7831446546fd9280878ff44e1e 100644
--- a/app/src/main/java/foundation/e/accountmanager/syncadapter/ContactsSyncAdapterService.kt
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/ContactsSyncAdapterService.kt
@@ -20,6 +20,10 @@ import foundation.e.accountmanager.resource.LocalAddressBook
import foundation.e.accountmanager.settings.AccountSettings
import java.util.logging.Level
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
class ContactsSyncAdapterService: SyncAdapterService() {
companion object {
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/ContactsSyncManager.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/ContactsSyncManager.kt
index 511cae93bb3e2919e33e9af0ed113262e39939f4..2875e9200f619a919ab8e0744b467a8416bf219b 100644
--- a/app/src/main/java/foundation/e/accountmanager/syncadapter/ContactsSyncManager.kt
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/ContactsSyncManager.kt
@@ -38,6 +38,8 @@ import java.io.*
import java.util.logging.Level
/**
+ * Authors: Nihar Thakkar and others
+ *
* Synchronization manager for CardDAV collections; handles contacts and groups.
*
* Group handling differs according to the {@link #groupMethod}. There are two basic methods to
@@ -88,6 +90,8 @@ class ContactsSyncManager(
}
private val readOnly = localAddressBook.readOnly
+ private val accessToken: String? = accountSettings.credentials().authState?.accessToken
+
private var hasVCard4 = false
private val groupMethod = accountSettings.getGroupMethod()
@@ -110,7 +114,7 @@ class ContactsSyncManager(
}
collectionURL = HttpUrl.parse(localCollection.url) ?: return false
- davCollection = DavAddressBook(httpClient.okHttpClient, collectionURL)
+ davCollection = DavAddressBook(httpClient.okHttpClient, collectionURL, accountSettings.credentials().authState?.accessToken)
resourceDownloader = ResourceDownloader(davCollection.location)
@@ -273,7 +277,7 @@ class ContactsSyncManager(
if (bunch.size == 1) {
val remote = bunch.first()
// only one contact, use GET
- useRemote(DavResource(httpClient.okHttpClient, remote)) { resource ->
+ useRemote(DavResource(httpClient.okHttpClient, remote, accountSettings.credentials().authState?.accessToken)) { resource ->
resource.get("text/vcard;version=4.0, text/vcard;charset=utf-8;q=0.8, text/vcard;q=0.5") { response ->
// CardDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc6352#section-6.3.2.3]
val eTag = response.header("ETag")?.let { GetETag(it).eTag }
@@ -437,10 +441,17 @@ class ContactsSyncManager(
val client = builder.build()
try {
- val response = client.okHttpClient.newCall(Request.Builder()
- .get()
- .url(httpUrl)
- .build()).execute()
+ val requestBuilder = Request.Builder()
+ .get()
+ .url(httpUrl)
+
+ if (accessToken!!.isNotEmpty()) {
+ requestBuilder.header("Authorization", "Bearer $accessToken")
+ }
+
+ val response = client.okHttpClient.newCall(requestBuilder
+ .build())
+ .execute()
if (response.isSuccessful)
return response.body()?.bytes()
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloAccountAuthenticatorService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloAccountAuthenticatorService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9c40c34d93b5599eef0034838a1078063466753b
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloAccountAuthenticatorService.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.*
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.database.DatabaseUtils
+import android.os.Bundle
+import androidx.annotation.WorkerThread
+import foundation.e.accountmanager.R
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.model.AppDatabase
+import foundation.e.accountmanager.resource.LocalAddressBook
+import foundation.e.accountmanager.ui.setup.LoginActivity
+import java.util.*
+import java.util.logging.Level
+import kotlin.concurrent.thread
+import kotlin.collections.ArrayList
+import kotlin.collections.HashSet
+
+/**
+ * Authors: Nihar Thakkar and others
+ *
+ * Account authenticator for the eelo account type.
+ *
+ * Gets started when an eelo account is removed, too, so it also watches for account removals
+ * and contains the corresponding cleanup code.
+ */
+class EeloAccountAuthenticatorService : Service(), OnAccountsUpdateListener {
+
+ companion object {
+
+ fun cleanupAccounts(context: Context) {
+ Logger.log.info("Cleaning up orphaned accounts")
+
+ val accountManager = AccountManager.get(context)
+ val accountNames = HashSet()
+
+ val accounts = ArrayList()
+ accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)).forEach { accounts.add(it) }
+ accountManager.getAccountsByType(context.getString(R.string.google_account_type)).forEach { accounts.add(it) }
+ accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { accounts.add(it) }
+ 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 == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT }
+
+
+ override fun onAccountsUpdated(accounts: Array?) {
+ thread {
+ cleanupAccounts(this)
+
+ val eeloAccounts = ArrayList(accounts?.asList()?: emptyList())
+ eeloAccounts.removeIf { it.type != getString(R.string.eelo_account_type) }
+ eeloAccounts.removeAll(accountManager.getAccountsByType(getString(
+ R.string.eelo_account_type)))
+ for (removedAccount in eeloAccounts) {
+ val intent = Intent("drive.services.ResetService")
+ intent.setPackage(getString(R.string.e_drive_package_name))
+ intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, removedAccount.name)
+ intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, removedAccount.type)
+ startService(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, LoginActivity::class.java)
+ intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
+ intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_EELO)
+ val bundle = Bundle(1)
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent)
+ return bundle
+ }
+
+ override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = null
+ override fun getAuthTokenLabel(p0: String?) = null
+ override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?) = null
+ override fun updateCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
+ override fun getAuthToken(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
+ override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?) = null
+
+ }
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloAddressBooksSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloAddressBooksSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1e928c4e53148a0a5ce0d306df6fd410a06c7ac5
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloAddressBooksSyncAdapterService.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.Manifest
+import android.accounts.Account
+import android.content.*
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.provider.ContactsContract
+import androidx.core.content.ContextCompat
+import foundation.e.accountmanager.closeCompat
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.model.AppDatabase
+import foundation.e.accountmanager.model.Collection
+import foundation.e.accountmanager.model.Service
+import foundation.e.accountmanager.resource.LocalAddressBook
+import foundation.e.accountmanager.settings.AccountSettings
+import foundation.e.accountmanager.ui.account.AccountActivity
+import okhttp3.HttpUrl
+import java.util.logging.Level
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class EeloAddressBooksSyncAdapterService : SyncAdapterService() {
+
+ override fun syncAdapter() = AddressBooksSyncAdapter(this)
+
+
+ class AddressBooksSyncAdapter(
+ context: Context
+ ) : SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ try {
+ val accountSettings = AccountSettings(context, account)
+
+ /* don't run sync if
+ - sync conditions (e.g. "sync only in WiFi") are not met AND
+ - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
+ */
+ if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
+ return
+
+ if (updateLocalAddressBooks(account, syncResult))
+ for (addressBookAccount in LocalAddressBook.findAll(context, null, account).map { it.account }) {
+ Logger.log.log(Level.INFO, "Running sync for address book", addressBookAccount)
+ val syncExtras = Bundle(extras)
+ syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true)
+ syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true)
+ ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras)
+ }
+ } catch (e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't sync address books", e)
+ }
+
+ Logger.log.info("Address book sync complete")
+ }
+
+ private fun updateLocalAddressBooks(account: Account, syncResult: SyncResult): Boolean {
+ val db = AppDatabase.getInstance(context)
+ val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CARDDAV)
+
+ val remoteAddressBooks = mutableMapOf()
+ if (service != null)
+ for (collection in db.collectionDao().getByServiceAndSync(service.id))
+ remoteAddressBooks[collection.url] = collection
+
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+ if (remoteAddressBooks.isEmpty())
+ Logger.log.info("No contacts permission, but no address book selected for synchronization")
+ else {
+ // no contacts permission, but address books should be synchronized -> show notification
+ val intent = Intent(context, AccountActivity::class.java)
+ intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ notifyPermissions(intent)
+ }
+ return false
+ }
+
+ val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
+ try {
+ if (contactsProvider == null) {
+ Logger.log.severe("Couldn't access contacts provider")
+ syncResult.databaseError = true
+ return false
+ }
+
+ // delete/update local address books
+ for (addressBook in LocalAddressBook.findAll(context, contactsProvider, account)) {
+ val url = HttpUrl.parse(addressBook.url)!!
+ val info = remoteAddressBooks[url]
+ if (info == null) {
+ Logger.log.log(Level.INFO, "Deleting obsolete local address book", url)
+ addressBook.delete()
+ } else {
+ // remote CollectionInfo found for this local collection, update data
+ try {
+ Logger.log.log(Level.FINE, "Updating local address book $url", info)
+ addressBook.update(info)
+ } catch (e: Exception) {
+ Logger.log.log(Level.WARNING, "Couldn't rename address book account", e)
+ }
+ // we already have a local address book for this remote collection, don't take into consideration anymore
+ remoteAddressBooks -= url
+ }
+ }
+
+ // create new local address books
+ for ((_, info) in remoteAddressBooks) {
+ Logger.log.log(Level.INFO, "Adding local address book", info)
+ LocalAddressBook.create(context, contactsProvider, account, info)
+ }
+ } finally {
+ contactsProvider?.closeCompat()
+ }
+
+ return true
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloAppDataSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloAppDataSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c986a214945057d8bc7d3124f461b6c9f1a0d825
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloAppDataSyncAdapterService.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.content.*
+import android.os.Bundle
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class EeloAppDataSyncAdapterService : SyncAdapterService() {
+
+ override fun syncAdapter() = CalendarsSyncAdapter(this)
+
+
+ class CalendarsSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ // Unused
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloCalendarsSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloCalendarsSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..299b0d30664aff47a0fac7cd06ab1f392757a40a
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloCalendarsSyncAdapterService.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.content.ContentProviderClient
+import android.content.ContentResolver
+import android.content.Context
+import android.content.SyncResult
+import android.os.Bundle
+import android.os.AsyncTask
+import android.provider.CalendarContract
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.model.AppDatabase
+import foundation.e.accountmanager.model.Collection
+import foundation.e.accountmanager.model.Credentials
+import foundation.e.accountmanager.model.Service
+import foundation.e.accountmanager.resource.LocalCalendar
+import foundation.e.accountmanager.settings.AccountSettings
+import foundation.e.ical4android.AndroidCalendar
+import net.openid.appauth.AuthorizationService
+import okhttp3.HttpUrl
+import java.util.logging.Level
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class EeloCalendarsSyncAdapterService: SyncAdapterService() {
+
+ override fun syncAdapter() = CalendarsSyncAdapter(this)
+
+
+ class CalendarsSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ try {
+ val accountSettings = AccountSettings(context, account)
+
+ /* don't run sync if
+ - sync conditions (e.g. "sync only in WiFi") are not met AND
+ - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
+ */
+ if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
+ return
+
+ if (accountSettings.getEventColors())
+ AndroidCalendar.insertColors(provider, account)
+ else
+ AndroidCalendar.removeColors(provider, account)
+
+ updateLocalCalendars(provider, account, accountSettings)
+
+ val priorityCalendars = priorityCollections(extras)
+ val calendars = AndroidCalendar
+ .find(account, provider, LocalCalendar.Factory, "${CalendarContract.Calendars.SYNC_EVENTS}!=0", null)
+ .sortedByDescending { priorityCalendars.contains(it.id) }
+ for (calendar in calendars) {
+ Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
+ CalendarSyncManager(context, account, accountSettings, extras, authority, syncResult, calendar).use {
+ val authState = accountSettings.credentials().authState
+ if (authState != null)
+ {
+ if (authState.needsTokenRefresh)
+ {
+ val tokenRequest = authState.createTokenRefreshRequest()
+
+ AuthorizationService(context).performTokenRequest(tokenRequest,
+ AuthorizationService.TokenResponseCallback { tokenResponse, ex ->
+ authState.update(tokenResponse, ex)
+ accountSettings.credentials(Credentials(account.name, null, authState, null))
+ it.accountSettings.credentials(Credentials(it.account.name, null, authState, null))
+ object : AsyncTask() {
+ override fun doInBackground(vararg params: Void): Void? {
+ it.performSync()
+ return null
+ }
+ }.execute()
+ })
+ }
+ else {
+ it.performSync()
+ }
+ }
+ else {
+ it.performSync()
+ }
+ }
+ }
+ } catch(e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't sync calendars", e)
+ }
+ Logger.log.info("Calendar sync complete")
+ }
+
+ private fun updateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
+ val db = AppDatabase.getInstance(context)
+ val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
+
+ val remoteCalendars = mutableMapOf()
+ if (service != null)
+ for (collection in db.collectionDao().getSyncCalendars(service.id)) {
+ remoteCalendars[collection.url] = collection
+ }
+
+ // delete/update local calendars
+ val updateColors = settings.getManageCalendarColors()
+ for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null))
+ calendar.name?.let {
+ val url = HttpUrl.parse(it)!!
+ val info = remoteCalendars[url]
+ if (info == null) {
+ Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url)
+ calendar.delete()
+ } else {
+ // remote CollectionInfo found for this local collection, update data
+ Logger.log.log(Level.FINE, "Updating local calendar $url", info)
+ calendar.update(info, updateColors)
+ // we already have a local calendar for this remote collection, don't take into consideration anymore
+ remoteCalendars -= url
+ }
+ }
+
+ // create new local calendars
+ for ((_, info) in remoteCalendars) {
+ Logger.log.log(Level.INFO, "Adding local calendar", info)
+ LocalCalendar.create(account, provider, info)
+ }
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloContactsSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloContactsSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f11de768ecad8cc523205b9b2a49b9b7fd508f0e
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloContactsSyncAdapterService.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.content.ContentProviderClient
+import android.content.ContentResolver
+import android.content.Context
+import android.content.SyncResult
+import android.os.Bundle
+import android.provider.ContactsContract
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.resource.LocalAddressBook
+import foundation.e.accountmanager.settings.AccountSettings
+import java.util.logging.Level
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class EeloContactsSyncAdapterService: SyncAdapterService() {
+
+ companion object {
+ const val PREVIOUS_GROUP_METHOD = "previous_group_method"
+ }
+
+ override fun syncAdapter() = ContactsSyncAdapter(this)
+
+
+ class ContactsSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ try {
+ val addressBook = LocalAddressBook(context, account, provider)
+ val accountSettings = AccountSettings(context, addressBook.mainAccount)
+
+ // handle group method change
+ val groupMethod = accountSettings.getGroupMethod().name
+ accountSettings.accountManager.getUserData(account, PREVIOUS_GROUP_METHOD)?.let { previousGroupMethod ->
+ if (previousGroupMethod != groupMethod) {
+ Logger.log.info("Group method changed, deleting all local contacts/groups")
+
+ // delete all local contacts and groups so that they will be downloaded again
+ provider.delete(addressBook.syncAdapterURI(ContactsContract.RawContacts.CONTENT_URI), null, null)
+ provider.delete(addressBook.syncAdapterURI(ContactsContract.Groups.CONTENT_URI), null, null)
+
+ // reset sync state
+ addressBook.syncState = null
+ }
+ }
+ accountSettings.accountManager.setUserData(account, PREVIOUS_GROUP_METHOD, groupMethod)
+
+ /* don't run sync if
+ - sync conditions (e.g. "sync only in WiFi") are not met AND
+ - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
+ */
+ if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
+ return
+
+ Logger.log.info("Synchronizing address book: ${addressBook.url}")
+ Logger.log.info("Taking settings from: ${addressBook.mainAccount}")
+
+ ContactsSyncManager(context, account, accountSettings, extras, authority, syncResult, provider, addressBook).use {
+ it.performSync()
+ }
+ } catch(e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't sync contacts", e)
+ }
+ Logger.log.info("Contacts sync complete")
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloEmailSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloEmailSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..201814c43b42a9825aaf4eb00d3329af10331daf
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloEmailSyncAdapterService.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.content.*
+import android.os.Bundle
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class EeloEmailSyncAdapterService : SyncAdapterService() {
+
+ override fun syncAdapter() = CalendarsSyncAdapter(this)
+
+
+ class CalendarsSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ // Unused
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloMediaSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloMediaSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e45e105f99d136174008cc45224857f7f5046800
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloMediaSyncAdapterService.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.content.*
+import android.os.Bundle
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class EeloMediaSyncAdapterService : SyncAdapterService() {
+
+ override fun syncAdapter() = CalendarsSyncAdapter(this)
+
+
+ class CalendarsSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ // Unused
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloNotesSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloNotesSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..097eb0b35ee861ed6cb675f74bf39484603b619a
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloNotesSyncAdapterService.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.content.*
+import android.os.Bundle
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class EeloNotesSyncAdapterService : SyncAdapterService() {
+
+ override fun syncAdapter() = CalendarsSyncAdapter(this)
+
+
+ class CalendarsSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ // Unused
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloNullAuthenticatorService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloNullAuthenticatorService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9f5c07f1ace3fa7f62ef3c03e04eb261dc4e1c04
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloNullAuthenticatorService.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.AbstractAccountAuthenticator
+import android.accounts.Account
+import android.accounts.AccountAuthenticatorResponse
+import android.accounts.AccountManager
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import foundation.e.accountmanager.ui.AccountsActivity
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class EeloNullAuthenticatorService: Service() {
+
+ private lateinit var accountAuthenticator: AccountAuthenticator
+
+ override fun onCreate() {
+ accountAuthenticator = AccountAuthenticator(this)
+ }
+
+ override fun onBind(intent: Intent?) =
+ accountAuthenticator.iBinder.takeIf { intent?.action == AccountManager.ACTION_AUTHENTICATOR_INTENT }
+
+
+ private class AccountAuthenticator(
+ val context: Context
+ ): AbstractAccountAuthenticator(context) {
+
+ override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?): Bundle {
+ val intent = Intent(context, AccountsActivity::class.java)
+ intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
+ val bundle = Bundle(1)
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent)
+ return bundle
+ }
+
+ override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = null
+ override fun getAuthTokenLabel(p0: String?) = null
+ override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?) = null
+ override fun updateCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
+ override fun getAuthToken(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
+ override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?) = null
+
+ }
+
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloTasksSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloTasksSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9216f202fd4c00d26a4445ea90f6ad7b8d3fd7c4
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/EeloTasksSyncAdapterService.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.app.PendingIntent
+import android.content.*
+import android.content.pm.PackageManager
+import android.graphics.drawable.BitmapDrawable
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.AsyncTask
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import foundation.e.accountmanager.R
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.model.AppDatabase
+import foundation.e.accountmanager.model.Collection
+import foundation.e.accountmanager.model.Credentials
+import foundation.e.accountmanager.model.Service
+import foundation.e.accountmanager.resource.LocalTaskList
+import foundation.e.accountmanager.settings.AccountSettings
+import foundation.e.accountmanager.ui.NotificationUtils
+import foundation.e.ical4android.AndroidTaskList
+import foundation.e.ical4android.TaskProvider
+import net.openid.appauth.AuthorizationService
+import okhttp3.HttpUrl
+import org.dmfs.tasks.contract.TaskContract
+import java.util.logging.Level
+
+/**
+ * Authors: Nihar Thakkar and others
+ *
+ * Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}).
+ */
+class EeloTasksSyncAdapterService: SyncAdapterService() {
+
+ override fun syncAdapter() = TasksSyncAdapter(this)
+
+
+ class TasksSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ try {
+ val taskProvider = TaskProvider.fromProviderClient(context, provider)
+
+ // make sure account can be seen by OpenTasks
+ if (Build.VERSION.SDK_INT >= 26)
+ AccountManager.get(context).setAccountVisibility(account, taskProvider.name.packageName, AccountManager.VISIBILITY_VISIBLE)
+
+ val accountSettings = AccountSettings(context, account)
+ /* don't run sync if
+ - sync conditions (e.g. "sync only in WiFi") are not met AND
+ - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
+ */
+ if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
+ return
+
+ updateLocalTaskLists(taskProvider, account, accountSettings)
+
+ val priorityTaskLists = priorityCollections(extras)
+ val taskLists = AndroidTaskList
+ .find(account, taskProvider, LocalTaskList.Factory, "${TaskContract.TaskLists.SYNC_ENABLED}!=0", null)
+ .sortedByDescending { priorityTaskLists.contains(it.id) }
+ for (taskList in taskLists) {
+ Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]")
+ TasksSyncManager(context, account, accountSettings, extras, authority, syncResult, taskList).use {
+ val authState = accountSettings.credentials().authState
+ if (authState != null)
+ {
+ if (authState.needsTokenRefresh)
+ {
+ val tokenRequest = authState.createTokenRefreshRequest()
+
+ AuthorizationService(context).performTokenRequest(tokenRequest,
+ AuthorizationService.TokenResponseCallback { tokenResponse, ex ->
+ authState.update(tokenResponse, ex)
+ accountSettings.credentials(Credentials(account.name, null, authState, null))
+ it.accountSettings.credentials(Credentials(it.account.name, null, authState, null))
+ object : AsyncTask() {
+ override fun doInBackground(vararg params: Void): Void? {
+ it.performSync()
+ return null
+ }
+ }.execute()
+ })
+ }
+ else {
+ it.performSync()
+ }
+ }
+ else {
+ it.performSync()
+ }
+ }
+ }
+ } catch (e: TaskProvider.ProviderTooOldException) {
+ val nm = NotificationManagerCompat.from(context)
+ val message = context.getString(R.string.sync_error_opentasks_required_version, e.provider.minVersionName, e.installedVersionName)
+ val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_ERRORS)
+ .setSmallIcon(R.drawable.ic_sync_problem_notify)
+ .setContentTitle(context.getString(R.string.sync_error_opentasks_too_old))
+ .setContentText(message)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(message))
+ .setCategory(NotificationCompat.CATEGORY_ERROR)
+
+ try {
+ val icon = context.packageManager.getApplicationIcon(e.provider.packageName)
+ if (icon is BitmapDrawable)
+ notify.setLargeIcon(icon.bitmap)
+ } catch(ignored: PackageManager.NameNotFoundException) {}
+
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${e.provider.packageName}"))
+ if (intent.resolveActivity(context.packageManager) != null)
+ notify .setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
+ .setAutoCancel(true)
+
+ nm.notify(NotificationUtils.NOTIFY_OPENTASKS, notify.build())
+ syncResult.databaseError = true
+ } catch (e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't sync task lists", e)
+ syncResult.databaseError = true
+ }
+
+ Logger.log.info("Task sync complete")
+ }
+
+ private fun updateLocalTaskLists(provider: TaskProvider, account: Account, settings: AccountSettings) {
+ val db = AppDatabase.getInstance(context)
+ val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
+
+ val remoteTaskLists = mutableMapOf()
+ if (service != null)
+ for (collection in db.collectionDao().getSyncTaskLists(service.id)) {
+ remoteTaskLists[collection.url] = collection
+ }
+
+ // delete/update local task lists
+ val updateColors = settings.getManageCalendarColors()
+
+ for (list in AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null))
+ list.syncId?.let {
+ val url = HttpUrl.parse(it)!!
+ val info = remoteTaskLists[url]
+ if (info == null) {
+ Logger.log.fine("Deleting obsolete local task list $url")
+ list.delete()
+ } else {
+ // remote CollectionInfo found for this local collection, update data
+ Logger.log.log(Level.FINE, "Updating local task list $url", info)
+ list.update(info, updateColors)
+ // we already have a local task list for this remote collection, don't take into consideration anymore
+ remoteTaskLists -= url
+ }
+ }
+
+ // create new local task lists
+ for ((_,info) in remoteTaskLists) {
+ Logger.log.log(Level.INFO, "Adding local task list", info)
+ LocalTaskList.create(account, provider, info)
+ }
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleAccountAuthenticatorService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleAccountAuthenticatorService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..92091f56723a71b36a6cc537178825f5ede65bd6
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleAccountAuthenticatorService.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.*
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.database.DatabaseUtils
+import android.os.Bundle
+import androidx.annotation.WorkerThread
+import foundation.e.accountmanager.R
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.model.AppDatabase
+import foundation.e.accountmanager.resource.LocalAddressBook
+import foundation.e.accountmanager.ui.setup.LoginActivity
+import java.util.*
+import java.util.logging.Level
+import kotlin.concurrent.thread
+import android.accounts.AccountManager
+import foundation.e.accountmanager.settings.AccountSettings
+import net.openid.appauth.AuthState
+import net.openid.appauth.AuthorizationService
+
+/**
+ * Authors: Nihar Thakkar and others
+ *
+ * Account authenticator for the Google account type.
+ *
+ * Gets started when a Google account is removed, too, so it also watches for account removals
+ * and contains the corresponding cleanup code.
+ */
+class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener {
+
+ companion object {
+
+ fun cleanupAccounts(context: Context) {
+ Logger.log.info("Cleaning up orphaned accounts")
+
+ val accountManager = AccountManager.get(context)
+ val accountNames = HashSet()
+ val accounts = ArrayList()
+ accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)).forEach { accounts.add(it) }
+ accountManager.getAccountsByType(context.getString(R.string.google_account_type)).forEach { accounts.add(it) }
+ accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { accounts.add(it) }
+ 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 == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT }
+
+
+ override fun onAccountsUpdated(accounts: Array?) {
+ thread {
+ cleanupAccounts(this)
+ }
+ }
+
+
+ private class AccountAuthenticator(
+ val context: Context
+ ) : AbstractAccountAuthenticator(context) {
+
+ override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?): Bundle {
+ val intent = Intent(context, LoginActivity::class.java)
+ intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
+ intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_GOOGLE)
+ val bundle = Bundle(1)
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent)
+ return bundle
+ }
+
+ override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = null
+ override fun getAuthTokenLabel(p0: String?) = null
+ override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?) = null
+ override fun updateCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
+ override fun getAuthToken(response: AccountAuthenticatorResponse?, account: Account?, authTokenType: String?, options: Bundle?): Bundle {
+ val accountManager = AccountManager.get(context)
+ val authState = AuthState.jsonDeserialize(accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE))
+
+ if (authState != null) {
+ if (authState.needsTokenRefresh) {
+ val tokenRequest = authState.createTokenRefreshRequest()
+
+ AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex ->
+ authState.update(tokenResponse, ex)
+ accountManager.setUserData(account, AccountSettings.KEY_AUTH_STATE, authState.jsonSerializeString())
+ val result = Bundle()
+ result.putString(AccountManager.KEY_ACCOUNT_NAME, account!!.name)
+ result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type)
+ result.putString(AccountManager.KEY_AUTHTOKEN, authState.accessToken)
+ response?.onResult(result)
+ }
+ }
+ else {
+ val result = Bundle()
+ result.putString(AccountManager.KEY_ACCOUNT_NAME, account!!.name)
+ result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type)
+ result.putString(AccountManager.KEY_AUTHTOKEN, authState.accessToken)
+ return result
+ }
+ }
+
+ val result = Bundle()
+ result.putInt(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION)
+ return result
+ }
+ override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?) = null
+
+ }
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleAddressBooksSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleAddressBooksSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3491737df4305a11884d23c09013865c551b7176
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleAddressBooksSyncAdapterService.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.Manifest
+import android.accounts.Account
+import android.content.*
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.provider.ContactsContract
+import androidx.core.content.ContextCompat
+import foundation.e.accountmanager.closeCompat
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.model.AppDatabase
+import foundation.e.accountmanager.model.Collection
+import foundation.e.accountmanager.model.Service
+import foundation.e.accountmanager.resource.LocalAddressBook
+import foundation.e.accountmanager.settings.AccountSettings
+import foundation.e.accountmanager.ui.account.AccountActivity
+import okhttp3.HttpUrl
+import java.util.logging.Level
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class GoogleAddressBooksSyncAdapterService : SyncAdapterService() {
+
+ override fun syncAdapter() = AddressBooksSyncAdapter(this)
+
+
+ class AddressBooksSyncAdapter(
+ context: Context
+ ) : SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ try {
+ val accountSettings = AccountSettings(context, account)
+
+ /* don't run sync if
+ - sync conditions (e.g. "sync only in WiFi") are not met AND
+ - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
+ */
+ if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
+ return
+
+ if (updateLocalAddressBooks(account, syncResult))
+ for (addressBookAccount in LocalAddressBook.findAll(context, null, account).map { it.account }) {
+ Logger.log.log(Level.INFO, "Running sync for address book", addressBookAccount)
+ val syncExtras = Bundle(extras)
+ syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true)
+ syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true)
+ ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras)
+ }
+ } catch (e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't sync address books", e)
+ }
+
+ Logger.log.info("Address book sync complete")
+ }
+
+ private fun updateLocalAddressBooks(account: Account, syncResult: SyncResult): Boolean {
+ val db = AppDatabase.getInstance(context)
+ val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CARDDAV)
+
+ val remoteAddressBooks = mutableMapOf()
+ if (service != null)
+ for (collection in db.collectionDao().getByServiceAndSync(service.id))
+ remoteAddressBooks[collection.url] = collection
+
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+ if (remoteAddressBooks.isEmpty())
+ Logger.log.info("No contacts permission, but no address book selected for synchronization")
+ else {
+ // no contacts permission, but address books should be synchronized -> show notification
+ val intent = Intent(context, AccountActivity::class.java)
+ intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ notifyPermissions(intent)
+ }
+ return false
+ }
+
+ val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
+ try {
+ if (contactsProvider == null) {
+ Logger.log.severe("Couldn't access contacts provider")
+ syncResult.databaseError = true
+ return false
+ }
+
+ // delete/update local address books
+ for (addressBook in LocalAddressBook.findAll(context, contactsProvider, account)) {
+ val url = HttpUrl.parse(addressBook.url)!!
+ val info = remoteAddressBooks[url]
+ if (info == null) {
+ Logger.log.log(Level.INFO, "Deleting obsolete local address book", url)
+ addressBook.delete()
+ } else {
+ // remote CollectionInfo found for this local collection, update data
+ try {
+ Logger.log.log(Level.FINE, "Updating local address book $url", info)
+ addressBook.update(info)
+ } catch (e: Exception) {
+ Logger.log.log(Level.WARNING, "Couldn't rename address book account", e)
+ }
+ // we already have a local address book for this remote collection, don't take into consideration anymore
+ remoteAddressBooks -= url
+ }
+ }
+
+ // create new local address books
+ for ((_, info) in remoteAddressBooks) {
+ Logger.log.log(Level.INFO, "Adding local address book", info)
+ LocalAddressBook.create(context, contactsProvider, account, info)
+ }
+ } finally {
+ contactsProvider?.closeCompat()
+ }
+
+ return true
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleCalendarsSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleCalendarsSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..dabe32fd4e6a2e681d5c69b88ddc438a8cd3c6c2
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleCalendarsSyncAdapterService.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.content.ContentProviderClient
+import android.content.ContentResolver
+import android.content.Context
+import android.content.SyncResult
+import android.os.Bundle
+import android.os.AsyncTask
+import android.provider.CalendarContract
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.model.AppDatabase
+import foundation.e.accountmanager.model.Collection
+import foundation.e.accountmanager.model.Credentials
+import foundation.e.accountmanager.model.Service
+import foundation.e.accountmanager.resource.LocalCalendar
+import foundation.e.accountmanager.settings.AccountSettings
+import foundation.e.ical4android.AndroidCalendar
+import net.openid.appauth.AuthorizationService
+import okhttp3.HttpUrl
+import java.util.logging.Level
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class GoogleCalendarsSyncAdapterService: SyncAdapterService() {
+
+ override fun syncAdapter() = CalendarsSyncAdapter(this)
+
+
+ class CalendarsSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ try {
+ val accountSettings = AccountSettings(context, account)
+
+ /* don't run sync if
+ - sync conditions (e.g. "sync only in WiFi") are not met AND
+ - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
+ */
+ if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
+ return
+
+ if (accountSettings.getEventColors())
+ AndroidCalendar.insertColors(provider, account)
+ else
+ AndroidCalendar.removeColors(provider, account)
+
+ updateLocalCalendars(provider, account, accountSettings)
+
+ val priorityCalendars = priorityCollections(extras)
+ val calendars = AndroidCalendar
+ .find(account, provider, LocalCalendar.Factory, "${CalendarContract.Calendars.SYNC_EVENTS}!=0", null)
+ .sortedByDescending { priorityCalendars.contains(it.id) }
+ for (calendar in calendars) {
+ Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
+ CalendarSyncManager(context, account, accountSettings, extras, authority, syncResult, calendar).use {
+ val authState = accountSettings.credentials().authState
+ if (authState != null)
+ {
+ if (authState.needsTokenRefresh)
+ {
+ val tokenRequest = authState.createTokenRefreshRequest()
+
+ AuthorizationService(context).performTokenRequest(tokenRequest,
+ AuthorizationService.TokenResponseCallback { tokenResponse, ex ->
+ authState.update(tokenResponse, ex)
+ accountSettings.credentials(Credentials(account.name, null, authState, null))
+ it.accountSettings.credentials(Credentials(it.account.name, null, authState, null))
+ object : AsyncTask() {
+ override fun doInBackground(vararg params: Void): Void? {
+ it.performSync()
+ return null
+ }
+ }.execute()
+ })
+ }
+ else {
+ it.performSync()
+ }
+ }
+ else {
+ it.performSync()
+ }
+ }
+ }
+ } catch(e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't sync calendars", e)
+ }
+ Logger.log.info("Calendar sync complete")
+ }
+
+ private fun updateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
+ val db = AppDatabase.getInstance(context)
+ val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
+
+ val remoteCalendars = mutableMapOf()
+ if (service != null)
+ for (collection in db.collectionDao().getSyncCalendars(service.id)) {
+ remoteCalendars[collection.url] = collection
+ }
+
+ // delete/update local calendars
+ val updateColors = settings.getManageCalendarColors()
+ for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null))
+ calendar.name?.let {
+ val url = HttpUrl.parse(it)!!
+ val info = remoteCalendars[url]
+ if (info == null) {
+ Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url)
+ calendar.delete()
+ } else {
+ // remote CollectionInfo found for this local collection, update data
+ Logger.log.log(Level.FINE, "Updating local calendar $url", info)
+ calendar.update(info, updateColors)
+ // we already have a local calendar for this remote collection, don't take into consideration anymore
+ remoteCalendars -= url
+ }
+ }
+
+ // create new local calendars
+ for ((_, info) in remoteCalendars) {
+ Logger.log.log(Level.INFO, "Adding local calendar", info)
+ LocalCalendar.create(account, provider, info)
+ }
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleContactsSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleContactsSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..25298e39d280e49b6b862c0faf8442a11313eb51
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleContactsSyncAdapterService.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.content.ContentProviderClient
+import android.content.ContentResolver
+import android.content.Context
+import android.content.SyncResult
+import android.os.Bundle
+import android.provider.ContactsContract
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.resource.LocalAddressBook
+import foundation.e.accountmanager.settings.AccountSettings
+import java.util.logging.Level
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class GoogleContactsSyncAdapterService: SyncAdapterService() {
+
+ companion object {
+ const val PREVIOUS_GROUP_METHOD = "previous_group_method"
+ }
+
+ override fun syncAdapter() = ContactsSyncAdapter(this)
+
+
+ class ContactsSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ try {
+ val addressBook = LocalAddressBook(context, account, provider)
+ val accountSettings = AccountSettings(context, addressBook.mainAccount)
+
+ // handle group method change
+ val groupMethod = accountSettings.getGroupMethod().name
+ accountSettings.accountManager.getUserData(account, PREVIOUS_GROUP_METHOD)?.let { previousGroupMethod ->
+ if (previousGroupMethod != groupMethod) {
+ Logger.log.info("Group method changed, deleting all local contacts/groups")
+
+ // delete all local contacts and groups so that they will be downloaded again
+ provider.delete(addressBook.syncAdapterURI(ContactsContract.RawContacts.CONTENT_URI), null, null)
+ provider.delete(addressBook.syncAdapterURI(ContactsContract.Groups.CONTENT_URI), null, null)
+
+ // reset sync state
+ addressBook.syncState = null
+ }
+ }
+ accountSettings.accountManager.setUserData(account, PREVIOUS_GROUP_METHOD, groupMethod)
+
+ /* don't run sync if
+ - sync conditions (e.g. "sync only in WiFi") are not met AND
+ - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
+ */
+ if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
+ return
+
+ Logger.log.info("Synchronizing address book: ${addressBook.url}")
+ Logger.log.info("Taking settings from: ${addressBook.mainAccount}")
+
+ ContactsSyncManager(context, account, accountSettings, extras, authority, syncResult, provider, addressBook).use {
+ it.performSync()
+ }
+ } catch(e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't sync contacts", e)
+ }
+ Logger.log.info("Contacts sync complete")
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleEmailSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleEmailSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2ff2a304ea0cfe8011b0f71ab4bf871c093319c7
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleEmailSyncAdapterService.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.content.*
+import android.os.Bundle
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class GoogleEmailSyncAdapterService : SyncAdapterService() {
+
+ override fun syncAdapter() = CalendarsSyncAdapter(this)
+
+
+ class CalendarsSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ // Unused
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleNullAuthenticatorService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleNullAuthenticatorService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9c8f0e05f845837bf412c6e568005c2dad823286
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleNullAuthenticatorService.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.AbstractAccountAuthenticator
+import android.accounts.Account
+import android.accounts.AccountAuthenticatorResponse
+import android.accounts.AccountManager
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import foundation.e.accountmanager.ui.AccountsActivity
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+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/foundation/e/accountmanager/syncadapter/GoogleTasksSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleTasksSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..230869675a1a7b6d2a29ca6e2c474e1a2dcaad5f
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/GoogleTasksSyncAdapterService.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+package foundation.e.accountmanager.syncadapter
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.app.PendingIntent
+import android.content.*
+import android.content.pm.PackageManager
+import android.graphics.drawable.BitmapDrawable
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.AsyncTask
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import foundation.e.accountmanager.R
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.model.AppDatabase
+import foundation.e.accountmanager.model.Collection
+import foundation.e.accountmanager.model.Credentials
+import foundation.e.accountmanager.model.Service
+import foundation.e.accountmanager.resource.LocalTaskList
+import foundation.e.accountmanager.settings.AccountSettings
+import foundation.e.accountmanager.ui.NotificationUtils
+import foundation.e.ical4android.AndroidTaskList
+import foundation.e.ical4android.TaskProvider
+import net.openid.appauth.AuthorizationService
+import okhttp3.HttpUrl
+import org.dmfs.tasks.contract.TaskContract
+import java.util.logging.Level
+
+/**
+ * Authors: Nihar Thakkar and others
+ *
+ * Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}).
+ */
+class GoogleTasksSyncAdapterService: SyncAdapterService() {
+
+ override fun syncAdapter() = TasksSyncAdapter(this)
+
+
+ class TasksSyncAdapter(
+ context: Context
+ ): SyncAdapter(context) {
+
+ override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
+ try {
+ val taskProvider = TaskProvider.fromProviderClient(context, provider)
+
+ // make sure account can be seen by OpenTasks
+ if (Build.VERSION.SDK_INT >= 26)
+ AccountManager.get(context).setAccountVisibility(account, taskProvider.name.packageName, AccountManager.VISIBILITY_VISIBLE)
+
+ val accountSettings = AccountSettings(context, account)
+ /* don't run sync if
+ - sync conditions (e.g. "sync only in WiFi") are not met AND
+ - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
+ */
+ if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
+ return
+
+ updateLocalTaskLists(taskProvider, account, accountSettings)
+
+ val priorityTaskLists = priorityCollections(extras)
+ val taskLists = AndroidTaskList
+ .find(account, taskProvider, LocalTaskList.Factory, "${TaskContract.TaskLists.SYNC_ENABLED}!=0", null)
+ .sortedByDescending { priorityTaskLists.contains(it.id) }
+ for (taskList in taskLists) {
+ Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]")
+ TasksSyncManager(context, account, accountSettings, extras, authority, syncResult, taskList).use {
+ val authState = accountSettings.credentials().authState
+ if (authState != null)
+ {
+ if (authState.needsTokenRefresh)
+ {
+ val tokenRequest = authState.createTokenRefreshRequest()
+
+ AuthorizationService(context).performTokenRequest(tokenRequest,
+ AuthorizationService.TokenResponseCallback { tokenResponse, ex ->
+ authState.update(tokenResponse, ex)
+ accountSettings.credentials(Credentials(account.name, null, authState, null))
+ it.accountSettings.credentials(Credentials(it.account.name, null, authState, null))
+ object : AsyncTask() {
+ override fun doInBackground(vararg params: Void): Void? {
+ it.performSync()
+ return null
+ }
+ }.execute()
+ })
+ }
+ else {
+ it.performSync()
+ }
+ }
+ else {
+ it.performSync()
+ }
+ }
+ }
+ } catch (e: TaskProvider.ProviderTooOldException) {
+ val nm = NotificationManagerCompat.from(context)
+ val message = context.getString(R.string.sync_error_opentasks_required_version, e.provider.minVersionName, e.installedVersionName)
+ val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_ERRORS)
+ .setSmallIcon(R.drawable.ic_sync_problem_notify)
+ .setContentTitle(context.getString(R.string.sync_error_opentasks_too_old))
+ .setContentText(message)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(message))
+ .setCategory(NotificationCompat.CATEGORY_ERROR)
+
+ try {
+ val icon = context.packageManager.getApplicationIcon(e.provider.packageName)
+ if (icon is BitmapDrawable)
+ notify.setLargeIcon(icon.bitmap)
+ } catch(ignored: PackageManager.NameNotFoundException) {}
+
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${e.provider.packageName}"))
+ if (intent.resolveActivity(context.packageManager) != null)
+ notify .setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
+ .setAutoCancel(true)
+
+ nm.notify(NotificationUtils.NOTIFY_OPENTASKS, notify.build())
+ syncResult.databaseError = true
+ } catch (e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't sync task lists", e)
+ syncResult.databaseError = true
+ }
+
+ Logger.log.info("Task sync complete")
+ }
+
+ private fun updateLocalTaskLists(provider: TaskProvider, account: Account, settings: AccountSettings) {
+ val db = AppDatabase.getInstance(context)
+ val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
+
+ val remoteTaskLists = mutableMapOf()
+ if (service != null)
+ for (collection in db.collectionDao().getSyncTaskLists(service.id)) {
+ remoteTaskLists[collection.url] = collection
+ }
+
+ // delete/update local task lists
+ val updateColors = settings.getManageCalendarColors()
+
+ for (list in AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null))
+ list.syncId?.let {
+ val url = HttpUrl.parse(it)!!
+ val info = remoteTaskLists[url]
+ if (info == null) {
+ Logger.log.fine("Deleting obsolete local task list $url")
+ list.delete()
+ } else {
+ // remote CollectionInfo found for this local collection, update data
+ Logger.log.log(Level.FINE, "Updating local task list $url", info)
+ list.update(info, updateColors)
+ // we already have a local task list for this remote collection, don't take into consideration anymore
+ remoteTaskLists -= url
+ }
+ }
+
+ // create new local task lists
+ for ((_,info) in remoteTaskLists) {
+ Logger.log.log(Level.INFO, "Adding local task list", info)
+ LocalTaskList.create(account, provider, info)
+ }
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/SyncManager.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/SyncManager.kt
index a2336633db024f348192626b1793677f01ea041f..86768d8f3085eda19bcc65a455cafeb38d1cecb8 100644
--- a/app/src/main/java/foundation/e/accountmanager/syncadapter/SyncManager.kt
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/SyncManager.kt
@@ -53,6 +53,10 @@ import java.util.logging.Level
import javax.net.ssl.SSLHandshakeException
import kotlin.math.min
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
@Suppress("MemberVisibilityCanBePrivate")
abstract class SyncManager, out CollectionType: LocalCollection, RemoteType: DavCollection>(
val context: Context,
@@ -288,7 +292,7 @@ abstract class SyncManager, out CollectionType: L
if (fileName != null) {
Logger.log.info("$fileName has been deleted locally -> deleting from server (ETag ${local.eTag})")
- useRemote(DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build())) { remote ->
+ useRemote(DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build(), accountSettings.credentials().authState?.accessToken)) { remote ->
try {
remote.delete(local.eTag) {}
numDeleted++
@@ -674,7 +678,10 @@ abstract class SyncManager, out CollectionType: L
val contentIntent: Intent
var viewItemAction: NotificationCompat.Action? = null
- if (e is UnauthorizedException) {
+ if ((account.type == context.getString(R.string.account_type) ||
+ account.type == context.getString(R.string.eelo_account_type) ||
+ account.type == context.getString(R.string.google_account_type)) &&
+ (e is UnauthorizedException || e is NotFoundException)) {
contentIntent = Intent(context, SettingsActivity::class.java)
contentIntent.putExtra(SettingsActivity.EXTRA_ACCOUNT,
if (authority == ContactsContract.AUTHORITY)
@@ -878,4 +885,4 @@ abstract class SyncManager, out CollectionType: L
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/TasksSyncAdapterService.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/TasksSyncAdapterService.kt
index 3263595751688b3c982bb3dc0fbdab5cb4628775..175d65519ac839e3a8bb8bdff399d9f5f9fc4646 100644
--- a/app/src/main/java/foundation/e/accountmanager/syncadapter/TasksSyncAdapterService.kt
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/TasksSyncAdapterService.kt
@@ -16,23 +16,28 @@ import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
+import android.os.AsyncTask
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import foundation.e.accountmanager.R
import foundation.e.accountmanager.log.Logger
import foundation.e.accountmanager.model.AppDatabase
import foundation.e.accountmanager.model.Collection
+import foundation.e.accountmanager.model.Credentials
import foundation.e.accountmanager.model.Service
import foundation.e.accountmanager.resource.LocalTaskList
import foundation.e.accountmanager.settings.AccountSettings
import foundation.e.accountmanager.ui.NotificationUtils
import foundation.e.ical4android.AndroidTaskList
import foundation.e.ical4android.TaskProvider
+import net.openid.appauth.AuthorizationService
import okhttp3.HttpUrl
import org.dmfs.tasks.contract.TaskContract
import java.util.logging.Level
/**
+ * Authors: Nihar Thakkar and others
+ *
* Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}).
*/
class TasksSyncAdapterService: SyncAdapterService() {
@@ -69,7 +74,33 @@ class TasksSyncAdapterService: SyncAdapterService() {
for (taskList in taskLists) {
Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]")
TasksSyncManager(context, account, accountSettings, extras, authority, syncResult, taskList).use {
- it.performSync()
+ val authState = accountSettings.credentials().authState
+ if (authState != null)
+ {
+ if (authState.needsTokenRefresh)
+ {
+ val tokenRequest = authState.createTokenRefreshRequest()
+
+ AuthorizationService(context).performTokenRequest(tokenRequest,
+ AuthorizationService.TokenResponseCallback { tokenResponse, ex ->
+ authState.update(tokenResponse, ex)
+ accountSettings.credentials(Credentials(account.name, null, authState, null))
+ it.accountSettings.credentials(Credentials(it.account.name, null, authState, null))
+ object : AsyncTask() {
+ override fun doInBackground(vararg params: Void): Void? {
+ it.performSync()
+ return null
+ }
+ }.execute()
+ })
+ }
+ else {
+ it.performSync()
+ }
+ }
+ else {
+ it.performSync()
+ }
}
}
} catch (e: TaskProvider.ProviderTooOldException) {
diff --git a/app/src/main/java/foundation/e/accountmanager/syncadapter/TasksSyncManager.kt b/app/src/main/java/foundation/e/accountmanager/syncadapter/TasksSyncManager.kt
index 8df5f67ab5fa7e531aca79a2f1b7e5c286a7f5e4..2290f8fe6651ac5d7d0f9ce02d13bda1a7944422 100644
--- a/app/src/main/java/foundation/e/accountmanager/syncadapter/TasksSyncManager.kt
+++ b/app/src/main/java/foundation/e/accountmanager/syncadapter/TasksSyncManager.kt
@@ -40,6 +40,8 @@ import java.io.StringReader
import java.util.logging.Level
/**
+ * Authors: Nihar Thakkar and others
+ *
* Synchronization manager for CalDAV collections; handles tasks (VTODO)
*/
class TasksSyncManager(
@@ -54,7 +56,7 @@ class TasksSyncManager(
override fun prepare(): Boolean {
collectionURL = HttpUrl.parse(localCollection.syncId ?: return false) ?: return false
- davCollection = DavCalendar(httpClient.okHttpClient, collectionURL)
+ davCollection = DavCalendar(httpClient.okHttpClient, collectionURL, accountSettings.credentials().authState?.accessToken)
return true
}
@@ -172,4 +174,4 @@ class TasksSyncManager(
override fun notifyInvalidResourceTitle(): String =
context.getString(R.string.sync_invalid_task)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/ui/AccountListFragment.kt b/app/src/main/java/foundation/e/accountmanager/ui/AccountListFragment.kt
index 50c44238a2f023fbb53e9e23db8dbbe757555851..7d13c9b966dbb904f32713d552a8776239ad9e99 100644
--- a/app/src/main/java/foundation/e/accountmanager/ui/AccountListFragment.kt
+++ b/app/src/main/java/foundation/e/accountmanager/ui/AccountListFragment.kt
@@ -38,6 +38,10 @@ import foundation.e.accountmanager.ui.account.AccountActivity
import kotlinx.android.synthetic.main.account_list.*
import kotlinx.android.synthetic.main.account_list_item.view.*
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
class AccountListFragment: ListFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@@ -162,11 +166,18 @@ class AccountListFragment: ListFragment() {
override fun onAccountsUpdated(newAccounts: Array) {
val context = getApplication()
+
+ 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(
- AccountManager.get(context).getAccountsByType(context.getString(R.string.account_type))
+ account.toTypedArray()
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/ui/AccountsActivity.kt b/app/src/main/java/foundation/e/accountmanager/ui/AccountsActivity.kt
index 0ed59fe1904b25c47c9daeb6dd4eb92a6c647b2c..85833bfcd16a2f573f1a82521afd389fc5e82d9c 100644
--- a/app/src/main/java/foundation/e/accountmanager/ui/AccountsActivity.kt
+++ b/app/src/main/java/foundation/e/accountmanager/ui/AccountsActivity.kt
@@ -26,6 +26,10 @@ import kotlinx.android.synthetic.main.accounts_content.*
import kotlinx.android.synthetic.main.activity_accounts.*
import kotlinx.android.synthetic.main.activity_accounts.view.*
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener, SyncStatusObserver {
companion object {
diff --git a/app/src/main/java/foundation/e/accountmanager/ui/DebugInfoActivity.kt b/app/src/main/java/foundation/e/accountmanager/ui/DebugInfoActivity.kt
index 1ced397dedee985d4b0eff16d473ba26c397d2ce..4bf0721868350aefc558607fa50e868871f3525b 100644
--- a/app/src/main/java/foundation/e/accountmanager/ui/DebugInfoActivity.kt
+++ b/app/src/main/java/foundation/e/accountmanager/ui/DebugInfoActivity.kt
@@ -55,6 +55,10 @@ import java.io.IOException
import java.util.logging.Level
import kotlin.concurrent.thread
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
class DebugInfoActivity: AppCompatActivity() {
companion object {
@@ -290,7 +294,12 @@ class DebugInfoActivity: AppCompatActivity() {
// main accounts
text.append("ACCOUNTS\n")
val accountManager = AccountManager.get(context)
- for (acct in accountManager.getAccountsByType(context.getString(R.string.account_type)))
+ 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 (acct in accounts.toTypedArray()) {
try {
val accountSettings = AccountSettings(context, acct)
text.append("Account: ${acct.name}\n" +
@@ -309,8 +318,10 @@ class DebugInfoActivity: AppCompatActivity() {
} catch (e: InvalidAccountException) {
text.append("$acct is invalid (unsupported settings version) or does not exist\n")
}
+ }
+
// address book accounts
- for (acct in accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)))
+ for (acct in accounts.toTypedArray())
try {
val addressBook = LocalAddressBook(context, acct, null)
text.append("Address book account: ${acct.name}\n" +
diff --git a/app/src/main/java/foundation/e/accountmanager/ui/DefaultAccountsDrawerHandler.kt b/app/src/main/java/foundation/e/accountmanager/ui/DefaultAccountsDrawerHandler.kt
index df113138c3a94ab01e25c76ead395e1e5a135634..019a4fa9b18420acc7d0e5e457dc8894d1033542 100644
--- a/app/src/main/java/foundation/e/accountmanager/ui/DefaultAccountsDrawerHandler.kt
+++ b/app/src/main/java/foundation/e/accountmanager/ui/DefaultAccountsDrawerHandler.kt
@@ -11,24 +11,19 @@ package foundation.e.accountmanager.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
-import android.net.Uri
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
-import foundation.e.accountmanager.App
-import foundation.e.accountmanager.BuildConfig
import foundation.e.accountmanager.R
-class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
-
- companion object {
- private const val BETA_FEEDBACK_URI = "mailto:play@bitfire.at?subject=${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} feedback (${BuildConfig.VERSION_CODE})"
- }
+/**
+ * Authors: Nihar Thakkar and others
+ */
+class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
override fun initMenu(context: Context, menu: Menu) {
- if (BuildConfig.VERSION_NAME.contains("-beta") || BuildConfig.VERSION_NAME.contains("-rc"))
- menu.findItem(R.id.nav_beta_feedback).isVisible = true
+ // TODO Provide option for beta feedback
}
override fun onNavigationItemSelected(activity: Activity, item: MenuItem): Boolean {
@@ -37,31 +32,6 @@ class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
activity.startActivity(Intent(activity, AboutActivity::class.java))
R.id.nav_app_settings ->
activity.startActivity(Intent(activity, AppSettingsActivity::class.java))
- R.id.nav_beta_feedback ->
- if (!UiUtils.launchUri(activity, Uri.parse(BETA_FEEDBACK_URI), Intent.ACTION_SENDTO, false))
- Toast.makeText(activity, R.string.install_email_client, Toast.LENGTH_LONG).show()
- R.id.nav_twitter ->
- UiUtils.launchUri(activity,
- Uri.parse("https://twitter.com/" + activity.getString(R.string.twitter_handle)))
- R.id.nav_website ->
- UiUtils.launchUri(activity,
- App.homepageUrl(activity))
- R.id.nav_manual ->
- UiUtils.launchUri(activity,
- App.homepageUrl(activity).buildUpon().appendPath("manual").build())
- R.id.nav_faq ->
- UiUtils.launchUri(activity,
- App.homepageUrl(activity).buildUpon().appendPath("faq").build())
- R.id.nav_forums ->
- UiUtils.launchUri(activity,
- App.homepageUrl(activity).buildUpon().appendPath("forums").build())
- R.id.nav_donate ->
- //if (BuildConfig.FLAVOR != App.FLAVOR_GOOGLE_PLAY)
- UiUtils.launchUri(activity,
- App.homepageUrl(activity).buildUpon().appendPath("donate").build())
- R.id.nav_privacy ->
- UiUtils.launchUri(activity,
- App.homepageUrl(activity).buildUpon().appendPath("privacy").build())
else ->
return false
}
diff --git a/app/src/main/java/foundation/e/accountmanager/ui/account/SettingsActivity.kt b/app/src/main/java/foundation/e/accountmanager/ui/account/SettingsActivity.kt
index 692dea35c23bd66c445555a32275d506ac248431..a6993008166367a3894ad90d040fef9bacfb2841 100644
--- a/app/src/main/java/foundation/e/accountmanager/ui/account/SettingsActivity.kt
+++ b/app/src/main/java/foundation/e/accountmanager/ui/account/SettingsActivity.kt
@@ -10,6 +10,7 @@ package foundation.e.accountmanager.ui.account
import android.Manifest
import android.accounts.Account
+import android.accounts.AccountManager
import android.app.Application
import android.content.ContentResolver
import android.content.Intent
@@ -91,6 +92,13 @@ class SettingsActivity: AppCompatActivity() {
addPreferencesFromResource(R.xml.settings_account)
}
+ private fun launchSetup():Boolean {
+ AccountManager.get(context).addAccount(getString(R.string.google_account_type),
+ null, null, null, activity, null,
+ null)
+ return true
+ }
+
private fun initSettings() {
//val accountSettings = AccountSettings(requireActivity(), account)
@@ -212,12 +220,14 @@ class SettingsActivity: AppCompatActivity() {
})
// preference group: authentication
+ val prefCredentials = findPreference("credentials")!!
val prefUserName = findPreference("username")!!
val prefPassword = findPreference("password")!!
val prefCertAlias = findPreference("certificate_alias")!!
model.credentials.observe(this, Observer { credentials ->
when (credentials.type) {
Credentials.Type.UsernamePassword -> {
+ prefCredentials.isVisible = false
prefUserName.isVisible = true
prefUserName.summary = credentials.userName
prefUserName.text = credentials.userName
@@ -235,6 +245,7 @@ class SettingsActivity: AppCompatActivity() {
prefCertAlias.isVisible = false
}
Credentials.Type.ClientCertificate -> {
+ prefCredentials.isVisible = false
prefUserName.isVisible = false
prefPassword.isVisible = false
@@ -247,6 +258,13 @@ class SettingsActivity: AppCompatActivity() {
true
}
}
+ Credentials.Type.OAuth -> {
+ prefCredentials.isVisible = true
+ prefCredentials.setOnPreferenceClickListener{launchSetup()}
+ prefUserName.isVisible = false
+ prefPassword.isVisible = false
+ prefCertAlias.isVisible = false
+ }
}
})
diff --git a/app/src/main/java/foundation/e/accountmanager/ui/setup/AccountDetailsFragment.kt b/app/src/main/java/foundation/e/accountmanager/ui/setup/AccountDetailsFragment.kt
index 40a9da0a36be2f5785e7f7c593905213494bbeb3..17fee7017a0b9f4dcb3365dffde4208e3e1425d2 100644
--- a/app/src/main/java/foundation/e/accountmanager/ui/setup/AccountDetailsFragment.kt
+++ b/app/src/main/java/foundation/e/accountmanager/ui/setup/AccountDetailsFragment.kt
@@ -9,7 +9,9 @@
package foundation.e.accountmanager.ui.setup
import android.accounts.Account
+import android.accounts.AccountAuthenticatorResponse
import android.accounts.AccountManager
+import android.app.Activity
import android.app.Application
import android.content.ContentResolver
import android.content.Intent
@@ -35,10 +37,15 @@ import foundation.e.accountmanager.settings.AccountSettings
import foundation.e.accountmanager.settings.Settings
import foundation.e.ical4android.TaskProvider
import foundation.e.vcard4android.GroupMethod
+import kotlinx.android.synthetic.main.login_account_details.view.*
import com.google.android.material.snackbar.Snackbar
import java.util.logging.Level
import kotlin.concurrent.thread
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
class AccountDetailsFragment: Fragment() {
private lateinit var loginModel: LoginModel
@@ -91,10 +98,43 @@ class AccountDetailsFragment: Fragment() {
config,
GroupMethod.valueOf(groupMethodName)
).observe(this, Observer { success ->
- if (success)
+ if (success) {
requireActivity().finish()
- else {
+ if (activity!!.intent.hasExtra(AccountManager
+ .KEY_ACCOUNT_AUTHENTICATOR_RESPONSE) && activity!!.intent
+ .getParcelableExtra(AccountManager
+ .KEY_ACCOUNT_AUTHENTICATOR_RESPONSE) != null) {
+ activity!!.intent
+ .getParcelableExtra(AccountManager
+ .KEY_ACCOUNT_AUTHENTICATOR_RESPONSE).onResult(null)
+ }
+ if (activity!!.intent.getStringExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE) == LoginActivity.ACCOUNT_PROVIDER_EELO) {
+ val intent = Intent("drive.services.InitializerService")
+ intent.setPackage(getString(R.string.e_drive_package_name))
+ intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, view!!.account_name.text.toString())
+ intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, getString(R.string.eelo_account_type))
+ activity!!.startService(intent)
+ }
+
+ } else {
Snackbar.make(requireActivity().findViewById(android.R.id.content), R.string.login_account_not_created, Snackbar.LENGTH_LONG).show()
+
+ if (activity!!.intent.hasExtra(AccountManager
+ .KEY_ACCOUNT_AUTHENTICATOR_RESPONSE) && activity!!.intent
+ .getParcelableExtra(AccountManager
+ .KEY_ACCOUNT_AUTHENTICATOR_RESPONSE) != null) {
+ activity!!.intent
+ .getParcelableExtra(AccountManager
+ .KEY_ACCOUNT_AUTHENTICATOR_RESPONSE).onResult(null)
+ }
+
+ if (activity!!.intent.getStringExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE) == LoginActivity.ACCOUNT_PROVIDER_EELO) {
+ val intent = Intent("drive.services.InitializerService")
+ intent.setPackage(getString(R.string.e_drive_package_name))
+ intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, view!!.account_name.text.toString())
+ intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, getString(R.string.eelo_account_type))
+ activity!!.startService(intent)
+ }
v.createAccountProgress.visibility = View.GONE
v.createAccount.visibility = View.VISIBLE
@@ -130,17 +170,64 @@ class AccountDetailsFragment: Fragment() {
val result = MutableLiveData()
val context = getApplication()
thread {
- val account = Account(name, context.getString(R.string.account_type))
+ 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()
+ }
+
+ val intent = Intent(context, LoginActivity::class.java)
+ when (intent.getStringExtra("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)
+ val userData = AccountSettings.initialUserData(credentials, baseURL)
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)) {
- result.postValue(false)
- return@thread
- }
+
+ 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 (!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)
// add entries for account to service DB
Logger.log.log(Level.INFO, "Writing account configuration to database", config)
@@ -154,7 +241,7 @@ class AccountDetailsFragment: Fragment() {
if (config.cardDAV != null) {
// insert CardDAV service
- val id = insertService(db, 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)
@@ -164,26 +251,26 @@ class AccountDetailsFragment: Fragment() {
context.startService(refreshIntent)
// 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_SYNC_INTERVAL)
+ accountSettings.setSyncInterval(context.getString(R.string.address_books_authority), Constants.DEFAULT_CALENDAR_SYNC_INTERVAL)
} else
ContentResolver.setIsSyncable(account, context.getString(R.string.address_books_authority), 0)
if (config.calDAV != null) {
// insert CalDAV service
- val id = insertService(db, 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)
// calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_calendars.xml
- accountSettings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL)
+ 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_SYNC_INTERVAL)
+ accountSettings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.authority, Constants.DEFAULT_CALENDAR_SYNC_INTERVAL)
}
} else {
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0)
@@ -200,9 +287,9 @@ class AccountDetailsFragment: Fragment() {
return result
}
- private fun insertService(db: AppDatabase, 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
@@ -223,4 +310,4 @@ class AccountDetailsFragment: Fragment() {
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/foundation/e/accountmanager/ui/setup/DavResourceFinder.kt b/app/src/main/java/foundation/e/accountmanager/ui/setup/DavResourceFinder.kt
index f3622dfb7443e6b3a59eefba891b2565e389518c..148ebbe8cc9b4c382cbf94e64a617878fee13bb0 100644
--- a/app/src/main/java/foundation/e/accountmanager/ui/setup/DavResourceFinder.kt
+++ b/app/src/main/java/foundation/e/accountmanager/ui/setup/DavResourceFinder.kt
@@ -31,6 +31,10 @@ import java.util.*
import java.util.logging.Level
import java.util.logging.Logger
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
class DavResourceFinder(
val context: Context,
private val loginModel: LoginModel
@@ -145,12 +149,13 @@ class DavResourceFinder(
if (config.principal != null && service == Service.CALDAV)
// query email address (CalDAV scheduling: calendar-user-address-set)
try {
- DavResource(httpClient.okHttpClient, config.principal!!, log).propfind(0, CalendarUserAddressSet.NAME) { response, _ ->
+ DavResource(httpClient.okHttpClient, config.principal!!, null, log).propfind(0, CalendarUserAddressSet.NAME) { response, _ ->
response[CalendarUserAddressSet::class.java]?.let { addressSet ->
for (href in addressSet.hrefs)
try {
val uri = URI(href)
if (uri.scheme.equals("mailto", true))
+ log.info("myenail: ${uri.schemeSpecificPart}")
config.email = uri.schemeSpecificPart
} catch(e: URISyntaxException) {
log.log(Level.WARNING, "Couldn't parse user address", e)
@@ -173,7 +178,7 @@ class DavResourceFinder(
private fun checkUserGivenURL(baseURL: HttpUrl, service: Service, config: Configuration.ServiceInfo) {
log.info("Checking user-given URL: $baseURL")
- val davBase = DavResource(httpClient.okHttpClient, baseURL, log)
+ val davBase = DavResource(httpClient.okHttpClient, baseURL, loginModel.credentials!!.authState!!.accessToken!!, log)
try {
when (service) {
Service.CARDDAV -> {
@@ -186,7 +191,7 @@ class DavResourceFinder(
}
}
Service.CALDAV -> {
- davBase.propfind(0,
+ davBase.propfind(1,
ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME, SupportedCalendarComponentSet.NAME,
CalendarHomeSet.NAME,
CurrentUserPrincipal.NAME
@@ -296,7 +301,7 @@ class DavResourceFinder(
fun providesService(url: HttpUrl, service: Service): Boolean {
var provided = false
try {
- DavResource(httpClient.okHttpClient, url, log).options { capabilities, _ ->
+ DavResource(httpClient.okHttpClient, url, loginModel.credentials!!.authState!!.accessToken!!, log).options { capabilities, _ ->
if ((service == Service.CARDDAV && capabilities.contains("addressbook")) ||
(service == Service.CALDAV && capabilities.contains("calendar-access")))
provided = true
@@ -382,7 +387,7 @@ class DavResourceFinder(
@Throws(IOException::class, HttpException::class, DavException::class)
fun getCurrentUserPrincipal(url: HttpUrl, service: Service?): HttpUrl? {
var principal: HttpUrl? = null
- DavResource(httpClient.okHttpClient, url, log).propfind(0, CurrentUserPrincipal.NAME) { response, _ ->
+ DavResource(httpClient.okHttpClient, url, loginModel.credentials!!.authState!!.accessToken!!, log).propfind(0, CurrentUserPrincipal.NAME) { response, _ ->
response[CurrentUserPrincipal::class.java]?.href?.let { href ->
response.requestedUrl.resolve(href)?.let {
log.info("Found current-user-principal: $it")
diff --git a/app/src/main/java/foundation/e/accountmanager/ui/setup/DefaultLoginCredentialsFragment.kt b/app/src/main/java/foundation/e/accountmanager/ui/setup/DefaultLoginCredentialsFragment.kt
index b5a0cdfdfb0c4d2972ef01aa2415513b157e3353..53283baafd11254d288f9b5645b3a3e1e59768e7 100644
--- a/app/src/main/java/foundation/e/accountmanager/ui/setup/DefaultLoginCredentialsFragment.kt
+++ b/app/src/main/java/foundation/e/accountmanager/ui/setup/DefaultLoginCredentialsFragment.kt
@@ -145,7 +145,7 @@ class DefaultLoginCredentialsFragment: Fragment() {
}
if (valid)
- loginModel.credentials = Credentials(null, null, alias)
+ loginModel.credentials = Credentials(null, null, null, alias)
}
}
diff --git a/app/src/main/java/foundation/e/accountmanager/ui/setup/DetectConfigurationFragment.kt b/app/src/main/java/foundation/e/accountmanager/ui/setup/DetectConfigurationFragment.kt
index 6fb468413d618581305e4ac7846089ecaa596f90..ae38a73bb799112f412501b25f0225e2b2214432 100644
--- a/app/src/main/java/foundation/e/accountmanager/ui/setup/DetectConfigurationFragment.kt
+++ b/app/src/main/java/foundation/e/accountmanager/ui/setup/DetectConfigurationFragment.kt
@@ -26,6 +26,10 @@ import java.lang.ref.WeakReference
import java.util.logging.Level
import kotlin.concurrent.thread
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
class DetectConfigurationFragment: Fragment() {
private lateinit var loginModel: LoginModel
diff --git a/app/src/main/java/foundation/e/accountmanager/ui/setup/EeloAuthenticatorFragment.kt b/app/src/main/java/foundation/e/accountmanager/ui/setup/EeloAuthenticatorFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1393d6f0d4d2c3e95f8fa0fa08fd5855f9c74382
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/ui/setup/EeloAuthenticatorFragment.kt
@@ -0,0 +1,180 @@
+package foundation.e.accountmanager.ui.setup
+
+import android.app.AlertDialog
+import android.content.Context
+import android.os.*
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.net.ConnectivityManager
+
+import foundation.e.accountmanager.R
+import androidx.lifecycle.ViewModelProviders
+import org.json.JSONObject
+import android.widget.Toast
+import android.net.Uri
+import android.widget.EditText
+import android.widget.LinearLayout
+import foundation.e.accountmanager.databinding.FragmentEeloAuthenticatorBinding
+import foundation.e.dav4jvm.Constants
+import java.net.IDN
+import java.net.URI
+import java.net.URISyntaxException
+import java.util.logging.Level
+import foundation.e.accountmanager.model.Credentials
+import kotlinx.android.synthetic.main.fragment_eelo_authenticator.*
+import kotlinx.android.synthetic.main.fragment_eelo_authenticator.view.*
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class EeloAuthenticatorFragment : Fragment() {
+
+ private lateinit var model: EeloAuthenticatorModel
+ private lateinit var loginModel: LoginModel
+
+ val TOGGLE_BUTTON_CHECKED_KEY = "toggle_button_checked"
+ var toggleButtonState = false
+
+ 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(EeloAuthenticatorModel::class.java)
+ loginModel = ViewModelProviders.of(requireActivity()).get(LoginModel::class.java)
+
+ val v = FragmentEeloAuthenticatorBinding.inflate(inflater, container, false)
+ v.lifecycleOwner = this
+ v.model = model
+
+ v.root.expand_collapse_button.setOnClickListener() { expandCollapse() }
+
+ v.root.urlpwd_user_name.setOnFocusChangeListener() { v, hasFocus ->
+ if (!hasFocus ) {
+ if (v.urlpwd_user_name.text.toString().contains("@")) {
+ val dns = v.urlpwd_user_name.text.toString().substringAfter("@")
+ val pre_custom_url = "https://" + dns
+ v.urlpwd_server_uri.setText(pre_custom_url)
+ } else {
+ v.urlpwd_server_uri.setText("")
+ }
+ }
+ }
+
+ v.login.setOnClickListener { login() }
+
+ // code below is to draw toggle button in its correct state and show or hide server url input field
+ //add by Vincent, 18/02/2019
+ if(savedInstanceState != null){
+ toggleButtonState = savedInstanceState.getBoolean(TOGGLE_BUTTON_CHECKED_KEY, false)
+ }
+
+ if(toggleButtonState == true) {
+ v.root.expand_collapse_button.setChecked(toggleButtonState)
+ v.root.urlpwd_server_uri_layout.setVisibility(View.VISIBLE)
+ v.root.urlpwd_server_uri.setEnabled(true)
+ }else{
+ v.root.urlpwd_server_uri_layout.setVisibility(View.GONE)
+ v.root.urlpwd_server_uri.setEnabled(false)
+ }
+
+ return v.root
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ outState.putBoolean(TOGGLE_BUTTON_CHECKED_KEY, toggleButtonState)
+ super.onSaveInstanceState(outState)
+ }
+
+ private fun login() {
+ if (!isNetworkAvailable()) {
+ Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
+ activity!!.finish()
+ }
+
+ if ((urlpwd_user_name.text.toString() != "") && (urlpwd_password.text.toString() != "")) {
+ if (validate())
+ requireFragmentManager().beginTransaction()
+ .replace(android.R.id.content, DetectConfigurationFragment(), null)
+ .addToBackStack(null)
+ .commit()
+ } else {
+ Toast.makeText(context, "Please enter a valid username and password", Toast.LENGTH_LONG).show()
+ }
+ }
+
+ private fun validate(): Boolean {
+ var valid = false
+
+ var serverUrl = model.baseUrl.value
+
+ fun validateUrl() {
+
+ if(toggleButtonState == true) {
+ serverUrl = view!!.urlpwd_server_uri.text.toString();
+ } else {
+ serverUrl = model.baseUrl.value
+ }
+
+ model.baseUrlError.value = null
+ try {
+ val uri = URI(serverUrl)
+ 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()
+
+ val userName = view!!.urlpwd_user_name.text.toString()
+ val password = view!!.urlpwd_password.text.toString()
+
+ if (loginModel.baseURI != null) {
+ valid = true
+ loginModel.credentials = Credentials(userName.toLowerCase(), password, null, null)
+ }
+ }
+
+ }
+
+ return valid
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ }
+
+ /**
+ * Show/Hide panel containing server's uri input field.
+ */
+ private fun expandCollapse(){
+ if(expand_collapse_button.isChecked) {
+ view!!.urlpwd_server_uri_layout.setVisibility(View.VISIBLE)
+ view!!.urlpwd_server_uri.setEnabled(true)
+ toggleButtonState = true;
+ }
+ else {
+ view!!.urlpwd_server_uri_layout.setVisibility(View.GONE)
+ view!!.urlpwd_server_uri.setEnabled(false)
+ toggleButtonState = false;
+ }
+ }
+
+
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/ui/setup/EeloAuthenticatorModel.kt b/app/src/main/java/foundation/e/accountmanager/ui/setup/EeloAuthenticatorModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..693445351dbed70deb6fe7df18c1f8964252ad6e
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/ui/setup/EeloAuthenticatorModel.kt
@@ -0,0 +1,54 @@
+package foundation.e.accountmanager.ui.setup
+
+import android.content.Intent
+import android.net.Uri
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+class EeloAuthenticatorModel: 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 = foundation.e.accountmanager.Constants.EELO_SYNC_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/foundation/e/accountmanager/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/foundation/e/accountmanager/ui/setup/GoogleAuthenticatorFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9b3a4a3409956fe3a708ca888f54faa474e738dd
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/ui/setup/GoogleAuthenticatorFragment.kt
@@ -0,0 +1,401 @@
+package foundation.e.accountmanager.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 foundation.e.dav4jvm.Constants
+
+import foundation.e.accountmanager.authorization.IdentityProvider
+
+import foundation.e.accountmanager.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 java.util.logging.Level
+import foundation.e.accountmanager.databinding.FragmentGoogleAuthenticatorBinding
+import foundation.e.accountmanager.model.Credentials
+import android.net.ConnectivityManager
+import android.widget.Toast
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+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)
+
+ if (!isNetworkAvailable()) {
+ Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
+ activity!!.finish()
+ }
+
+ // Initialise the authorization service
+ authorizationService = AuthorizationService(context!!)
+
+ val v = FragmentGoogleAuthenticatorBinding.inflate(inflater, container, false)
+ v.lifecycleOwner = this
+ v.model = model
+
+ activity?.intent?.let {
+ if (!it.getBooleanExtra(LoginActivity.ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE, false)) {
+ // 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 {
+ 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)
+ }
+ }
+ }
+ else {
+ val response = AuthorizationResponse.fromIntent(activity!!.intent)
+ val ex = AuthorizationException.fromIntent(activity!!.intent)
+ authState = AuthState(response, ex)
+
+ if (response != null) {
+ exchangeAuthorizationCode(response)
+ }
+ 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())
+ 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(): Boolean {
+ var valid = false
+
+ fun validateUrl() {
+ model.baseUrlError.value = null
+ try {
+ val uri = URI(model.baseUrl.value.orEmpty())
+ 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()
+
+ val email = model.emailAddress.value
+
+ if (loginModel.baseURI != null) {
+ valid = true
+ loginModel.credentials = Credentials(email, null, authState!!, null)
+ }
+ }
+
+ }
+
+ return valid
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ authorizationService?.dispose()
+ }
+
+
+}
+
diff --git a/app/src/main/java/foundation/e/accountmanager/ui/setup/GoogleAuthenticatorModel.kt b/app/src/main/java/foundation/e/accountmanager/ui/setup/GoogleAuthenticatorModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c6dc6af32e74ce4c02a6c4c4062a6634b0ef2838
--- /dev/null
+++ b/app/src/main/java/foundation/e/accountmanager/ui/setup/GoogleAuthenticatorModel.kt
@@ -0,0 +1,54 @@
+package foundation.e.accountmanager.ui.setup
+
+import android.content.Intent
+import android.net.Uri
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+/**
+ * Authors: Nihar Thakkar and others
+ */
+
+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 = "https://apidata.googleusercontent.com/caldav/v2/$emailAddress/user"
+ 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/foundation/e/accountmanager/ui/setup/LoginActivity.kt b/app/src/main/java/foundation/e/accountmanager/ui/setup/LoginActivity.kt
index 9fa991a2ceb291e6760591d6c82d9366c63d6e15..ec7e415b5db10a373483b434b380f33a5125cedd 100644
--- a/app/src/main/java/foundation/e/accountmanager/ui/setup/LoginActivity.kt
+++ b/app/src/main/java/foundation/e/accountmanager/ui/setup/LoginActivity.kt
@@ -9,17 +9,17 @@
package foundation.e.accountmanager.ui.setup
import android.os.Bundle
-import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import foundation.e.accountmanager.App
-import foundation.e.accountmanager.R
import foundation.e.accountmanager.log.Logger
import foundation.e.accountmanager.ui.UiUtils
import java.util.*
/**
+ * Authors: Nihar Thakkar and others
+ *
* Activity to initially connect to a server and create an account.
* Fields for server/user data can be pre-filled with extras in the Intent.
*/
@@ -42,6 +42,11 @@ class LoginActivity: AppCompatActivity() {
* When set, the password field will be set to this value.
*/
const val EXTRA_PASSWORD = "password"
+
+ 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"
}
private val loginFragmentLoader = ServiceLoader.load(ILoginCredentialsFragment::class.java)!!
@@ -57,22 +62,36 @@ class LoginActivity: AppCompatActivity() {
fragment = fragment ?: factory.getFragment(intent)
if (fragment != null) {
- supportFragmentManager.beginTransaction()
- .replace(android.R.id.content, fragment)
- .commit()
+ when (intent.getStringExtra(SETUP_ACCOUNT_PROVIDER_TYPE)) {
+ ACCOUNT_PROVIDER_EELO -> {
+ supportFragmentManager.beginTransaction()
+ .replace(android.R.id.content, EeloAuthenticatorFragment())
+ .commit()
+ }
+ ACCOUNT_PROVIDER_GOOGLE -> {
+ supportFragmentManager.beginTransaction()
+ .replace(android.R.id.content, GoogleAuthenticatorFragment())
+ .commit()
+ }
+ else ->
+ // first call, add first login fragment
+ supportFragmentManager.beginTransaction()
+ .replace(android.R.id.content, fragment)
+ .commit()
+ }
} else
Logger.log.severe("Couldn't create LoginFragment")
}
}
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.activity_login, menu)
- return true
- }
-
- fun showHelp(item: MenuItem) {
- UiUtils.launchUri(this,
- App.homepageUrl(this).buildUpon().appendPath("tested-with").build())
+ override fun onOptionsItemSelected(item: MenuItem?): Boolean {
+ when {
+ item?.itemId == android.R.id.home -> {
+ finish()
+ return true
+ }
+ }
+ return false
}
}
diff --git a/app/src/main/res/drawable-xxhdpi/ic_account_provider_eelo.png b/app/src/main/res/drawable-xxhdpi/ic_account_provider_eelo.png
new file mode 100644
index 0000000000000000000000000000000000000000..be7f21f840662fa3658879dd7a963de5640f4d31
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_account_provider_eelo.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_account_provider_google.png b/app/src/main/res/drawable-xxhdpi/ic_account_provider_google.png
new file mode 100644
index 0000000000000000000000000000000000000000..2154694f964da71ead68a6ff4baa784a21e76a03
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_account_provider_google.png differ
diff --git a/app/src/main/res/drawable/custom_url_button_value.xml b/app/src/main/res/drawable/custom_url_button_value.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fd4b1bbeb6a59950bf42ef0f6afd2ed27ec4e7d0
--- /dev/null
+++ b/app/src/main/res/drawable/custom_url_button_value.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_expand_less.xml b/app/src/main/res/drawable/ic_expand_less.xml
new file mode 100644
index 0000000000000000000000000000000000000000..30a106fec9749c057e507837c9bf7a2dcb31e5dc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_expand_less.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_expand_more.xml b/app/src/main/res/drawable/ic_expand_more.xml
new file mode 100644
index 0000000000000000000000000000000000000000..77316f76e6b5c438b29976c1e704168e2d63c837
--- /dev/null
+++ b/app/src/main/res/drawable/ic_expand_more.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_eelo_authenticator.xml b/app/src/main/res/layout/fragment_eelo_authenticator.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ce3f1c152ee52d7b7bf90d9d1c080c562687b299
--- /dev/null
+++ b/app/src/main/res/layout/fragment_eelo_authenticator.xml
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 0000000000000000000000000000000000000000..a8a293b8db06edd4774c64ff979bc688db16d0cf
--- /dev/null
+++ b/app/src/main/res/layout/fragment_google_authenticator.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/login_account_details.xml b/app/src/main/res/layout/login_account_details.xml
index 59b9e0efe8d8977977c0c98aac94ab72ddfca3c1..6926edad26f19b4fd8a057ed3e4b79951cac8618 100644
--- a/app/src/main/res/layout/login_account_details.xml
+++ b/app/src/main/res/layout/login_account_details.xml
@@ -45,7 +45,8 @@
android:layout_height="wrap_content"
android:hint="@string/login_account_name">
-
\ No newline at end of file
+
diff --git a/app/src/main/res/menu/activity_accounts_drawer.xml b/app/src/main/res/menu/activity_accounts_drawer.xml
index 181cb3243668d859d5f1a64c707fcdcb9e4f7c6c..c07b44e81997f86a810d6190a6086bbbfb326941 100644
--- a/app/src/main/res/menu/activity_accounts_drawer.xml
+++ b/app/src/main/res/menu/activity_accounts_drawer.xml
@@ -7,62 +7,16 @@
~ http://www.gnu.org/licenses/gpl.html
-->
-