mirror of
https://github.com/etesync/android
synced 2025-02-13 16:12:21 +00:00
Merge: add EteSync 2.0 support
This commit is contained in:
commit
d4ef9f7fe3
@ -7,9 +7,9 @@
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
@ -21,8 +21,8 @@ android {
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 29
|
||||
|
||||
versionCode 114
|
||||
versionName "1.16.2"
|
||||
versionCode 200
|
||||
versionName "2.0.0"
|
||||
|
||||
buildConfigField "boolean", "customCerts", "true"
|
||||
}
|
||||
@ -134,6 +134,8 @@ dependencies {
|
||||
implementation "org.jetbrains.anko:anko-commons:0.10.4"
|
||||
|
||||
implementation "com.etesync:journalmanager:1.1.1"
|
||||
def etebaseVersion = '0.2.0'
|
||||
implementation "com.etebase:client:$etebaseVersion"
|
||||
|
||||
def acraVersion = '5.3.0'
|
||||
implementation "ch.acra:acra-mail:$acraVersion"
|
||||
@ -141,9 +143,16 @@ dependencies {
|
||||
def supportVersion = '1.0.0'
|
||||
implementation "androidx.legacy:legacy-support-core-ui:$supportVersion"
|
||||
implementation "androidx.core:core:$supportVersion"
|
||||
implementation "androidx.fragment:fragment:$supportVersion"
|
||||
implementation "androidx.appcompat:appcompat:1.1.0"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
// KTX extensions
|
||||
implementation "androidx.core:core-ktx:1.3.1"
|
||||
implementation "androidx.fragment:fragment-ktx:1.2.5"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
|
||||
|
||||
|
||||
implementation 'com.google.android.material:material:1.2.0-beta01'
|
||||
implementation "androidx.legacy:legacy-preference-v14:$supportVersion"
|
||||
implementation 'com.github.yukuku:ambilwarna:2.0.1'
|
||||
|
@ -127,7 +127,7 @@
|
||||
<!-- Address book account -->
|
||||
<service
|
||||
android:name=".syncadapter.NullAuthenticatorService"
|
||||
android:exported="true"> <!-- Since Android 11, this must be true so that Google Contacts shows the address book accounts -->
|
||||
android:exported="true"> <!-- Since Android 11, this must be true so that Google Contacts shows the address book accounts -->
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator"/>
|
||||
</intent-filter>
|
||||
@ -233,6 +233,14 @@
|
||||
android:exported="false"
|
||||
android:parentActivityName=".ui.AccountsActivity">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.etebase.CollectionActivity"
|
||||
android:exported="false"
|
||||
/>
|
||||
<activity
|
||||
android:name=".ui.etebase.InvitationsActivity"
|
||||
android:exported="false"
|
||||
/>
|
||||
<activity
|
||||
android:name=".ui.ViewCollectionActivity"
|
||||
android:exported="false"
|
||||
|
@ -36,8 +36,12 @@ constructor(internal val context: Context, internal val account: Account) {
|
||||
|
||||
var uri: URI?
|
||||
get() {
|
||||
val uri = accountManager.getUserData(account, KEY_URI)
|
||||
if (uri == null) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return URI(accountManager.getUserData(account, KEY_URI))
|
||||
return URI(uri)
|
||||
} catch (e: URISyntaxException) {
|
||||
return null
|
||||
}
|
||||
@ -73,6 +77,12 @@ constructor(internal val context: Context, internal val account: Account) {
|
||||
get() = accountManager.getUserData(account, KEY_WIFI_ONLY_SSID)
|
||||
set(ssid) = accountManager.setUserData(account, KEY_WIFI_ONLY_SSID, ssid)
|
||||
|
||||
var etebaseSession: String?
|
||||
get() = accountManager.getUserData(account, KEY_ETEBASE_SESSION)
|
||||
set(value) = accountManager.setUserData(account, KEY_ETEBASE_SESSION, value)
|
||||
|
||||
val isLegacy: Boolean
|
||||
get() = authToken != null
|
||||
|
||||
// CalDAV settings
|
||||
|
||||
@ -216,6 +226,7 @@ constructor(internal val context: Context, internal val account: Account) {
|
||||
private val KEY_ASYMMETRIC_PRIVATE_KEY = "asymmetric_private_key"
|
||||
private val KEY_ASYMMETRIC_PUBLIC_KEY = "asymmetric_public_key"
|
||||
private val KEY_WIFI_ONLY = "wifi_only"
|
||||
private val KEY_ETEBASE_SESSION = "etebase_session"
|
||||
// sync on WiFi only (default: false)
|
||||
private val KEY_WIFI_ONLY_SSID = "wifi_only_ssid" // restrict sync to specific WiFi SSID
|
||||
|
||||
@ -243,10 +254,10 @@ constructor(internal val context: Context, internal val account: Account) {
|
||||
val SYNC_INTERVAL_MANUALLY: Long = -1
|
||||
|
||||
// XXX: Workaround a bug in Android where passing a bundle to addAccountExplicitly doesn't work.
|
||||
fun setUserData(accountManager: AccountManager, account: Account, uri: URI, userName: String) {
|
||||
fun setUserData(accountManager: AccountManager, account: Account, uri: URI?, userName: String) {
|
||||
accountManager.setUserData(account, KEY_SETTINGS_VERSION, CURRENT_VERSION.toString())
|
||||
accountManager.setUserData(account, KEY_USERNAME, userName)
|
||||
accountManager.setUserData(account, KEY_URI, uri.toString())
|
||||
accountManager.setUserData(account, KEY_URI, uri?.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -158,22 +158,6 @@ class App : Application() {
|
||||
private fun update(fromVersion: Int) {
|
||||
Logger.log.info("Updating from version " + fromVersion + " to " + BuildConfig.VERSION_CODE)
|
||||
|
||||
if (fromVersion < 6) {
|
||||
val data = this.data
|
||||
|
||||
val dbHelper = ServiceDB.OpenHelper(this)
|
||||
|
||||
val collections = readCollections(dbHelper)
|
||||
for (info in collections) {
|
||||
val journalEntity = JournalEntity(data, info)
|
||||
data.insert(journalEntity)
|
||||
}
|
||||
|
||||
val db = dbHelper.writableDatabase
|
||||
db.delete(ServiceDB.Collections._TABLE, null, null)
|
||||
db.close()
|
||||
}
|
||||
|
||||
if (fromVersion < 7) {
|
||||
/* Fix all of the etags to be non-null */
|
||||
val am = AccountManager.get(this)
|
||||
@ -234,21 +218,6 @@ class App : Application() {
|
||||
|
||||
}
|
||||
|
||||
private fun readCollections(dbHelper: ServiceDB.OpenHelper): List<CollectionInfo> {
|
||||
val db = dbHelper.writableDatabase
|
||||
val collections = LinkedList<CollectionInfo>()
|
||||
val cursor = db.query(ServiceDB.Collections._TABLE, null, null, null, null, null, null)
|
||||
while (cursor.moveToNext()) {
|
||||
val values = ContentValues()
|
||||
DatabaseUtils.cursorRowToContentValues(cursor, values)
|
||||
collections.add(CollectionInfo.fromDB(values))
|
||||
}
|
||||
|
||||
db.close()
|
||||
cursor.close()
|
||||
return collections
|
||||
}
|
||||
|
||||
fun migrateServices(dbHelper: ServiceDB.OpenHelper) {
|
||||
val db = dbHelper.readableDatabase
|
||||
val data = this.data
|
||||
|
@ -32,9 +32,10 @@ public class Constants {
|
||||
public static final Uri dashboard = webUri.buildUpon().appendEncodedPath("dashboard/").build();
|
||||
public static final Uri faqUri = webUri.buildUpon().appendEncodedPath("faq/").build();
|
||||
public static final Uri helpUri = webUri.buildUpon().appendEncodedPath("user-guide/android/").build();
|
||||
public static final Uri forgotPassword = webUri.buildUpon().appendEncodedPath("accounts/password/reset/").build();
|
||||
public static final Uri forgotPassword = faqUri.buildUpon().fragment("forgot-password").build();
|
||||
|
||||
public static final Uri serviceUrl = Uri.parse((DEBUG_REMOTE_URL == null) ? "https://api.etesync.com/" : DEBUG_REMOTE_URL);
|
||||
public static final String etebaseServiceUrl = (DEBUG_REMOTE_URL == null) ? "https://api.etebase.com/partner/etesync/" : DEBUG_REMOTE_URL;
|
||||
|
||||
public static final String PRODID_BASE = "-//EteSync//" + BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_NAME;
|
||||
|
||||
@ -43,4 +44,8 @@ public class Constants {
|
||||
|
||||
public final static String KEY_ACCOUNT = "account",
|
||||
KEY_COLLECTION_INFO = "collectionInfo";
|
||||
|
||||
public final static String ETEBASE_TYPE_ADDRESS_BOOK = "etebase.vcard";
|
||||
public final static String ETEBASE_TYPE_CALENDAR = "etebase.vevent";
|
||||
public final static String ETEBASE_TYPE_TASKS = "etebase.vtodo";
|
||||
}
|
||||
|
166
app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt
Normal file
166
app/src/main/java/com/etesync/syncadapter/EtebaseLocalCache.kt
Normal file
@ -0,0 +1,166 @@
|
||||
package com.etesync.syncadapter
|
||||
|
||||
import android.content.Context
|
||||
import com.etebase.client.*
|
||||
import com.etebase.client.Collection
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
/*
|
||||
File structure:
|
||||
cache_dir/
|
||||
user1/ <--- the name of the user
|
||||
stoken <-- the stokens of the collection fetch
|
||||
cols/
|
||||
UID1/ - The uid of the first col
|
||||
...
|
||||
UID2/ - The uid of the second col
|
||||
col <-- the col itself
|
||||
stoken <-- the stoken of the items fetch
|
||||
items/
|
||||
item_uid1 <-- the item with uid 1
|
||||
item_uid2
|
||||
...
|
||||
*/
|
||||
class EtebaseLocalCache private constructor(context: Context, username: String) {
|
||||
private val filesDir: File = File(context.filesDir, username)
|
||||
private val colsDir: File
|
||||
|
||||
init {
|
||||
colsDir = File(filesDir, "cols")
|
||||
colsDir.mkdirs()
|
||||
}
|
||||
|
||||
private fun getCollectionItemsDir(colUid: String): File {
|
||||
val colsDir = File(filesDir, "cols")
|
||||
val colDir = File(colsDir, colUid)
|
||||
return File(colDir, "items")
|
||||
}
|
||||
|
||||
fun clearUserCache() {
|
||||
filesDir.deleteRecursively()
|
||||
}
|
||||
|
||||
fun saveStoken(stoken: String) {
|
||||
val stokenFile = File(filesDir, "stoken")
|
||||
stokenFile.writeText(stoken)
|
||||
}
|
||||
|
||||
fun loadStoken(): String? {
|
||||
val stokenFile = File(filesDir, "stoken")
|
||||
return if (stokenFile.exists()) stokenFile.readText() else null
|
||||
}
|
||||
|
||||
|
||||
fun collectionSaveStoken(colUid: String, stoken: String) {
|
||||
val colDir = File(colsDir, colUid)
|
||||
val stokenFile = File(colDir, "stoken")
|
||||
stokenFile.writeText(stoken)
|
||||
}
|
||||
|
||||
fun collectionLoadStoken(colUid: String): String? {
|
||||
val colDir = File(colsDir, colUid)
|
||||
val stokenFile = File(colDir, "stoken")
|
||||
return if (stokenFile.exists()) stokenFile.readText() else null
|
||||
}
|
||||
|
||||
fun collectionList(colMgr: CollectionManager, withDeleted: Boolean = false): List<CachedCollection> {
|
||||
return colsDir.list().map {
|
||||
val colDir = File(colsDir, it)
|
||||
val colFile = File(colDir, "col")
|
||||
val content = colFile.readBytes()
|
||||
colMgr.cacheLoad(content)
|
||||
}.filter { withDeleted || !it.isDeleted }.map{
|
||||
CachedCollection(it, it.meta)
|
||||
}
|
||||
}
|
||||
|
||||
fun collectionGet(colMgr: CollectionManager, colUid: String): CachedCollection? {
|
||||
val colDir = File(colsDir, colUid)
|
||||
val colFile = File(colDir, "col")
|
||||
if (!colFile.exists()) {
|
||||
return null
|
||||
}
|
||||
val content = colFile.readBytes()
|
||||
return colMgr.cacheLoad(content).let {
|
||||
CachedCollection(it, it.meta)
|
||||
}
|
||||
}
|
||||
|
||||
fun collectionSet(colMgr: CollectionManager, collection: Collection) {
|
||||
val colDir = File(colsDir, collection.uid)
|
||||
colDir.mkdirs()
|
||||
val colFile = File(colDir, "col")
|
||||
colFile.writeBytes(colMgr.cacheSaveWithContent(collection))
|
||||
val itemsDir = getCollectionItemsDir(collection.uid)
|
||||
itemsDir.mkdirs()
|
||||
}
|
||||
|
||||
fun collectionUnset(colMgr: CollectionManager, colUid: String) {
|
||||
val colDir = File(colsDir, colUid)
|
||||
colDir.deleteRecursively()
|
||||
}
|
||||
|
||||
fun itemList(itemMgr: ItemManager, colUid: String, withDeleted: Boolean = false): List<CachedItem> {
|
||||
val itemsDir = getCollectionItemsDir(colUid)
|
||||
return itemsDir.list().map {
|
||||
val itemFile = File(itemsDir, it)
|
||||
val content = itemFile.readBytes()
|
||||
itemMgr.cacheLoad(content)
|
||||
}.filter { withDeleted || !it.isDeleted }.map {
|
||||
CachedItem(it, it.meta, it.contentString)
|
||||
}
|
||||
}
|
||||
|
||||
fun itemGet(itemMgr: ItemManager, colUid: String, itemUid: String): CachedItem? {
|
||||
val itemsDir = getCollectionItemsDir(colUid)
|
||||
val itemFile = File(itemsDir, itemUid)
|
||||
if (!itemFile.exists()) {
|
||||
return null
|
||||
}
|
||||
val content = itemFile.readBytes()
|
||||
return itemMgr.cacheLoad(content).let {
|
||||
CachedItem(it, it.meta, it.contentString)
|
||||
}
|
||||
}
|
||||
|
||||
fun itemSet(itemMgr: ItemManager, colUid: String, item: Item) {
|
||||
val itemsDir = getCollectionItemsDir(colUid)
|
||||
val itemFile = File(itemsDir, item.uid)
|
||||
itemFile.writeBytes(itemMgr.cacheSaveWithContent(item))
|
||||
}
|
||||
|
||||
fun itemUnset(itemMgr: ItemManager, colUid: String, itemUid: String) {
|
||||
val itemsDir = getCollectionItemsDir(colUid)
|
||||
val itemFile = File(itemsDir, itemUid)
|
||||
itemFile.delete()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val localCacheCache: HashMap<String, EtebaseLocalCache> = HashMap()
|
||||
|
||||
fun getInstance(context: Context, username: String): EtebaseLocalCache {
|
||||
synchronized(localCacheCache) {
|
||||
val cached = localCacheCache.get(username)
|
||||
if (cached != null) {
|
||||
return cached
|
||||
} else {
|
||||
val ret = EtebaseLocalCache(context, username)
|
||||
localCacheCache.set(username, ret)
|
||||
return ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: If we ever cache this we need to cache bust on changePassword
|
||||
fun getEtebase(context: Context, httpClient: OkHttpClient, settings: AccountSettings): Account {
|
||||
val client = Client.create(httpClient, settings.uri?.toString())
|
||||
return Account.restore(client, settings.etebaseSession!!, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class CachedCollection(val col: Collection, val meta: CollectionMetadata)
|
||||
|
||||
data class CachedItem(val item: Item, val meta: ItemMetadata, val content: String)
|
@ -68,7 +68,6 @@ class HttpClient private constructor(
|
||||
) {
|
||||
private var certManager: CustomCertManager? = null
|
||||
private var certificateAlias: String? = null
|
||||
private var cache: Cache? = null
|
||||
|
||||
private val orig = sharedClient.newBuilder()
|
||||
|
||||
|
@ -48,23 +48,6 @@ class CollectionInfo : com.etesync.journalmanager.model.CollectionInfo() {
|
||||
return info
|
||||
}
|
||||
|
||||
fun fromDB(values: ContentValues): CollectionInfo {
|
||||
val info = CollectionInfo()
|
||||
info.id = values.getAsLong(Collections.ID)!!
|
||||
info.serviceID = values.getAsInteger(Collections.SERVICE_ID)!!
|
||||
|
||||
info.uid = values.getAsString(Collections.URL)
|
||||
info.displayName = values.getAsString(Collections.DISPLAY_NAME)
|
||||
info.description = values.getAsString(Collections.DESCRIPTION)
|
||||
|
||||
info.color = values.getAsInteger(Collections.COLOR)
|
||||
|
||||
info.timeZone = values.getAsString(Collections.TIME_ZONE)
|
||||
|
||||
info.selected = values.getAsInteger(Collections.SYNC) != 0
|
||||
return info
|
||||
}
|
||||
|
||||
fun fromJson(json: String): CollectionInfo {
|
||||
return GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, CollectionInfo::class.java)
|
||||
}
|
||||
|
@ -19,7 +19,9 @@ import android.provider.ContactsContract.CommonDataKinds.GroupMembership
|
||||
import android.provider.ContactsContract.Groups
|
||||
import android.provider.ContactsContract.RawContacts
|
||||
import at.bitfire.vcard4android.*
|
||||
import com.etebase.client.CollectionAccessLevel
|
||||
import com.etesync.syncadapter.App
|
||||
import com.etesync.syncadapter.CachedCollection
|
||||
import com.etesync.syncadapter.log.Logger
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
import com.etesync.syncadapter.model.JournalEntity
|
||||
@ -70,6 +72,37 @@ class LocalAddressBook(
|
||||
return addressBook
|
||||
}
|
||||
|
||||
fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, cachedCollection: CachedCollection): LocalAddressBook {
|
||||
val col = cachedCollection.col
|
||||
val accountManager = AccountManager.get(context)
|
||||
|
||||
val account = Account(accountName(mainAccount, cachedCollection), App.addressBookAccountType)
|
||||
val userData = initialUserData(mainAccount, col.uid)
|
||||
Logger.log.log(Level.INFO, "Creating local address book $account", userData)
|
||||
if (!accountManager.addAccountExplicitly(account, null, userData))
|
||||
throw IllegalStateException("Couldn't create address book account")
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
// Android < 7 seems to lose the initial user data sometimes, so set it a second time
|
||||
// https://forums.bitfire.at/post/11644
|
||||
userData.keySet().forEach { key ->
|
||||
accountManager.setUserData(account, key, userData.getString(key))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val addressBook = LocalAddressBook(context, account, provider)
|
||||
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1)
|
||||
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
|
||||
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
|
||||
addressBook.settings = values
|
||||
addressBook.readOnly = col.accessLevel == CollectionAccessLevel.ReadOnly
|
||||
|
||||
return addressBook
|
||||
}
|
||||
|
||||
fun find(context: Context, provider: ContentProviderClient?, mainAccount: Account?) = AccountManager.get(context)
|
||||
.getAccountsByType(App.addressBookAccountType)
|
||||
@ -103,6 +136,13 @@ class LocalAddressBook(
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun accountName(mainAccount: Account, cachedCollection: CachedCollection): String {
|
||||
val col = cachedCollection.col
|
||||
val meta = cachedCollection.meta
|
||||
val displayName = meta.name
|
||||
return "${displayName} (${mainAccount.name} ${col.uid.substring(0, 2)})"
|
||||
}
|
||||
|
||||
fun initialUserData(mainAccount: Account, url: String): Bundle {
|
||||
val bundle = Bundle(3)
|
||||
bundle.putString(USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
|
||||
@ -181,6 +221,37 @@ class LocalAddressBook(
|
||||
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
|
||||
}
|
||||
|
||||
|
||||
fun update(cachedCollection: CachedCollection) {
|
||||
val col = cachedCollection.col
|
||||
val newAccountName = accountName(mainAccount, cachedCollection)
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
if (account.name != newAccountName && Build.VERSION.SDK_INT >= 21) {
|
||||
val accountManager = AccountManager.get(context)
|
||||
val future = accountManager.renameAccount(account, newAccountName, {
|
||||
try {
|
||||
// update raw contacts to new account name
|
||||
if (provider != null) {
|
||||
val values = ContentValues(1)
|
||||
values.put(RawContacts.ACCOUNT_NAME, newAccountName)
|
||||
provider.update(syncAdapterURI(RawContacts.CONTENT_URI), values, RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?",
|
||||
arrayOf(account.name, account.type))
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
Logger.log.log(Level.WARNING, "Couldn't re-assign contacts to new account name", e)
|
||||
}
|
||||
}, null)
|
||||
account = future.result
|
||||
}
|
||||
|
||||
readOnly = col.accessLevel == CollectionAccessLevel.ReadOnly
|
||||
Logger.log.info("Address book write permission? = ${!readOnly}")
|
||||
|
||||
// make sure it will still be synchronized when contacts are updated
|
||||
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
|
||||
}
|
||||
|
||||
fun delete() {
|
||||
val accountManager = AccountManager.get(context)
|
||||
|
||||
@ -279,6 +350,16 @@ class LocalAddressBook(
|
||||
}
|
||||
}
|
||||
|
||||
override fun findByFilename(filename: String): LocalAddress? {
|
||||
val found = queryContacts("${AndroidContact.COLUMN_FILENAME}=?", arrayOf(filename)).firstOrNull()
|
||||
|
||||
if (found != null) {
|
||||
return found
|
||||
} else {
|
||||
return queryGroups("${AndroidGroup.COLUMN_FILENAME}=?", arrayOf(filename)).firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun findGroupById(id: Long): LocalGroup =
|
||||
queryGroups("${Groups._ID}=?", arrayOf(id.toString())).firstOrNull()
|
||||
?: throw FileNotFoundException()
|
||||
|
@ -18,9 +18,13 @@ import android.os.RemoteException
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.CalendarContract.*
|
||||
import at.bitfire.ical4android.*
|
||||
import com.etebase.client.CollectionAccessLevel
|
||||
import com.etesync.syncadapter.CachedCollection
|
||||
import com.etesync.syncadapter.log.Logger
|
||||
import com.etesync.syncadapter.model.JournalEntity
|
||||
import com.etesync.syncadapter.resource.LocalEvent.Companion.COLUMN_UID
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.dmfs.tasks.contract.TaskContract
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
@ -31,7 +35,21 @@ class LocalCalendar private constructor(
|
||||
): AndroidCalendar<LocalEvent>(account, provider, LocalEvent.Factory, id), LocalCollection<LocalEvent> {
|
||||
|
||||
companion object {
|
||||
val defaultColor = -0x743cb6 // light green 500
|
||||
val defaultColor = -0x743cb6 // light green 500 - should be "8BC349"?
|
||||
|
||||
fun parseColor(color_: String?): Int {
|
||||
if (color_.isNullOrBlank()) {
|
||||
return defaultColor
|
||||
}
|
||||
val color = color_.replaceFirst("^#".toRegex(), "")
|
||||
if (color.length == 8) {
|
||||
return (color.substring(0, 6).toLong(16) or (color.substring(6, 8).toLong(16) shl 24)).toInt()
|
||||
} else if (color.length == 6) {
|
||||
return (color.toLong(16) or (0xFF000000)).toInt()
|
||||
} else {
|
||||
return defaultColor
|
||||
}
|
||||
}
|
||||
|
||||
val COLUMN_CTAG = Calendars.CAL_SYNC1
|
||||
|
||||
@ -50,6 +68,21 @@ class LocalCalendar private constructor(
|
||||
return AndroidCalendar.create(account, provider, values)
|
||||
}
|
||||
|
||||
fun create(account: Account, provider: ContentProviderClient, cachedCollection: CachedCollection): Uri {
|
||||
val values = valuesFromCachedCollection(cachedCollection, true)
|
||||
|
||||
// ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash.
|
||||
values.put(Calendars.ACCOUNT_NAME, account.name)
|
||||
values.put(Calendars.ACCOUNT_TYPE, account.type)
|
||||
values.put(Calendars.OWNER_ACCOUNT, account.name)
|
||||
|
||||
// flag as visible & synchronizable at creation, might be changed by user at any time
|
||||
values.put(Calendars.VISIBLE, 1)
|
||||
values.put(Calendars.SYNC_EVENTS, 1)
|
||||
|
||||
return AndroidCalendar.create(account, provider, values)
|
||||
}
|
||||
|
||||
fun findByName(account: Account, provider: ContentProviderClient, factory: Factory, name: String): LocalCalendar?
|
||||
= AndroidCalendar.find(account, provider, factory, Calendars.NAME + "==?", arrayOf(name)).firstOrNull()
|
||||
|
||||
@ -85,6 +118,30 @@ class LocalCalendar private constructor(
|
||||
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(intArrayOf(CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE), ", "))
|
||||
return values
|
||||
}
|
||||
|
||||
private fun valuesFromCachedCollection(cachedCollection: CachedCollection, withColor: Boolean): ContentValues {
|
||||
val values = ContentValues()
|
||||
val col = cachedCollection.col
|
||||
val meta = cachedCollection.meta
|
||||
values.put(Calendars.NAME, col.uid)
|
||||
values.put(Calendars.CALENDAR_DISPLAY_NAME, meta.name)
|
||||
|
||||
if (withColor)
|
||||
values.put(Calendars.CALENDAR_COLOR, parseColor(meta.color))
|
||||
|
||||
if (col.accessLevel == CollectionAccessLevel.ReadOnly)
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
|
||||
else {
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
|
||||
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1)
|
||||
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1)
|
||||
}
|
||||
|
||||
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT)
|
||||
values.put(Calendars.ALLOWED_AVAILABILITY, StringUtils.join(intArrayOf(Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY), ","))
|
||||
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(intArrayOf(CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE), ", "))
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
override val url: String?
|
||||
@ -93,6 +150,9 @@ class LocalCalendar private constructor(
|
||||
fun update(journalEntity: JournalEntity, updateColor: Boolean) =
|
||||
update(valuesFromCollectionInfo(journalEntity, updateColor))
|
||||
|
||||
fun update(cachedCollection: CachedCollection, updateColor: Boolean) =
|
||||
update(valuesFromCachedCollection(cachedCollection, updateColor))
|
||||
|
||||
|
||||
override fun findDeleted() =
|
||||
queryEvents("${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)
|
||||
@ -122,7 +182,10 @@ class LocalCalendar private constructor(
|
||||
= queryEvents(null, null)
|
||||
|
||||
override fun findByUid(uid: String): LocalEvent?
|
||||
= queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull()
|
||||
= queryEvents(COLUMN_UID + " =? ", arrayOf(uid)).firstOrNull()
|
||||
|
||||
override fun findByFilename(filename: String): LocalEvent?
|
||||
= queryEvents(Events._SYNC_ID + " =? ", arrayOf(filename)).firstOrNull()
|
||||
|
||||
fun processDirtyExceptions() {
|
||||
// process deleted exceptions
|
||||
|
@ -17,6 +17,7 @@ interface LocalCollection<out T: LocalResource<*>> {
|
||||
fun findAll(): List<T>
|
||||
|
||||
fun findByUid(uid: String): T?
|
||||
fun findByFilename(filename: String): T?
|
||||
|
||||
|
||||
fun count(): Long
|
||||
|
@ -52,7 +52,7 @@ class LocalContact : AndroidContact, LocalAddress {
|
||||
|
||||
override// The same now
|
||||
val uuid: String?
|
||||
get() = fileName
|
||||
get() = contact?.uid
|
||||
|
||||
override val isLocalOnly: Boolean
|
||||
get() = TextUtils.isEmpty(eTag)
|
||||
@ -88,9 +88,11 @@ class LocalContact : AndroidContact, LocalAddress {
|
||||
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
|
||||
}
|
||||
|
||||
override fun clearDirty(eTag: String) {
|
||||
override fun clearDirty(eTag: String?) {
|
||||
val values = ContentValues(3)
|
||||
values.put(AndroidContact.COLUMN_ETAG, eTag)
|
||||
if (eTag != null) {
|
||||
values.put(AndroidContact.COLUMN_ETAG, eTag)
|
||||
}
|
||||
values.put(ContactsContract.RawContacts.DIRTY, 0)
|
||||
|
||||
if (LocalContact.HASH_HACK) {
|
||||
@ -105,15 +107,26 @@ class LocalContact : AndroidContact, LocalAddress {
|
||||
this.eTag = eTag
|
||||
}
|
||||
|
||||
override fun prepareForUpload() {
|
||||
override fun legacyPrepareForUpload(fileName_: String?) {
|
||||
val uid = UUID.randomUUID().toString()
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(AndroidContact.COLUMN_FILENAME, uid)
|
||||
val fileName = fileName_ ?: uid
|
||||
values.put(AndroidContact.COLUMN_FILENAME, fileName)
|
||||
values.put(AndroidContact.COLUMN_UID, uid)
|
||||
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
|
||||
|
||||
fileName = uid
|
||||
this.fileName = fileName
|
||||
}
|
||||
|
||||
override fun prepareForUpload(fileName: String, uid: String) {
|
||||
val values = ContentValues(2)
|
||||
values.put(AndroidContact.COLUMN_FILENAME, fileName)
|
||||
values.put(AndroidContact.COLUMN_UID, uid)
|
||||
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
|
||||
|
||||
contact?.uid = uid
|
||||
this.fileName = fileName
|
||||
}
|
||||
|
||||
override fun populateData(mimeType: String, row: ContentValues) {
|
||||
|
@ -38,14 +38,14 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
|
||||
|
||||
private var saveAsDirty = false // When true, the resource will be saved as dirty
|
||||
|
||||
private var fileName: String? = null
|
||||
override var fileName: String? = null
|
||||
var eTag: String? = null
|
||||
|
||||
var weAreOrganizer = true
|
||||
|
||||
override val content: String
|
||||
get() {
|
||||
Logger.log.log(Level.FINE, "Preparing upload of event " + fileName!!, event)
|
||||
Logger.log.log(Level.FINE, "Preparing upload of event $fileName} ${event}")
|
||||
|
||||
val os = ByteArrayOutputStream()
|
||||
event?.write(os)
|
||||
@ -58,7 +58,7 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
|
||||
|
||||
override// Now the same
|
||||
val uuid: String?
|
||||
get() = fileName
|
||||
get() = event?.uid
|
||||
|
||||
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?) : super(calendar, event) {
|
||||
this.fileName = fileName
|
||||
@ -133,7 +133,7 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
|
||||
|
||||
/* custom queries */
|
||||
|
||||
override fun prepareForUpload() {
|
||||
override fun legacyPrepareForUpload(fileName_: String?) {
|
||||
var uid: String? = null
|
||||
val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null)
|
||||
if (c.moveToNext())
|
||||
@ -142,30 +142,42 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
|
||||
uid = UUID.randomUUID().toString()
|
||||
|
||||
c.close()
|
||||
val newFileName = uid
|
||||
|
||||
val fileName = fileName_ ?: uid
|
||||
val values = ContentValues(2)
|
||||
values.put(Events._SYNC_ID, newFileName)
|
||||
values.put(Events._SYNC_ID, fileName)
|
||||
values.put(COLUMN_UID, uid)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
fileName = newFileName
|
||||
this.fileName = fileName
|
||||
|
||||
val event = this.event
|
||||
if (event != null)
|
||||
event.uid = uid
|
||||
}
|
||||
|
||||
override fun prepareForUpload(fileName: String, uid: String) {
|
||||
val values = ContentValues(2)
|
||||
values.put(Events._SYNC_ID, fileName)
|
||||
values.put(COLUMN_UID, uid)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
event?.uid = uid
|
||||
this.fileName = fileName
|
||||
}
|
||||
|
||||
override fun resetDeleted() {
|
||||
val values = ContentValues(1)
|
||||
values.put(CalendarContract.Events.DELETED, 0)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
}
|
||||
|
||||
override fun clearDirty(eTag: String) {
|
||||
override fun clearDirty(eTag: String?) {
|
||||
val values = ContentValues(2)
|
||||
values.put(CalendarContract.Events.DIRTY, 0)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
if (eTag != null) {
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
}
|
||||
if (event != null)
|
||||
values.put(COLUMN_SEQUENCE, event?.sequence)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
@ -120,13 +120,15 @@ class LocalGroup : AndroidGroup, LocalAddress {
|
||||
return values
|
||||
}
|
||||
|
||||
override fun clearDirty(eTag: String) {
|
||||
override fun clearDirty(eTag: String?) {
|
||||
val id = requireNotNull(id)
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(Groups.DIRTY, 0)
|
||||
this.eTag = eTag
|
||||
values.put(AndroidGroup.COLUMN_ETAG, eTag)
|
||||
if (eTag != null) {
|
||||
values.put(AndroidGroup.COLUMN_ETAG, eTag)
|
||||
}
|
||||
update(values)
|
||||
|
||||
// update cached group memberships
|
||||
@ -154,15 +156,26 @@ class LocalGroup : AndroidGroup, LocalAddress {
|
||||
batch.commit()
|
||||
}
|
||||
|
||||
override fun prepareForUpload() {
|
||||
override fun legacyPrepareForUpload(fileName_: String?) {
|
||||
val uid = UUID.randomUUID().toString()
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(AndroidGroup.COLUMN_FILENAME, uid)
|
||||
val fileName = fileName_ ?: uid
|
||||
values.put(AndroidGroup.COLUMN_FILENAME, fileName)
|
||||
values.put(AndroidGroup.COLUMN_UID, uid)
|
||||
update(values)
|
||||
|
||||
fileName = uid
|
||||
this.fileName = fileName
|
||||
}
|
||||
|
||||
override fun prepareForUpload(fileName: String, uid: String) {
|
||||
val values = ContentValues(2)
|
||||
values.put(AndroidGroup.COLUMN_FILENAME, fileName)
|
||||
values.put(AndroidGroup.COLUMN_UID, uid)
|
||||
addressBook.provider?.update(groupSyncUri(), values, null, null)
|
||||
|
||||
contact?.uid = uid
|
||||
this.fileName = fileName
|
||||
}
|
||||
|
||||
override fun resetDeleted() {
|
||||
|
@ -10,6 +10,7 @@ package com.etesync.syncadapter.resource
|
||||
|
||||
interface LocalResource<in TData: Any> {
|
||||
val uuid: String?
|
||||
val fileName: String?
|
||||
|
||||
/** True if doesn't exist on server yet, false otherwise. */
|
||||
val isLocalOnly: Boolean
|
||||
@ -19,9 +20,11 @@ interface LocalResource<in TData: Any> {
|
||||
|
||||
fun delete(): Int
|
||||
|
||||
fun prepareForUpload()
|
||||
fun legacyPrepareForUpload(fileName: String?)
|
||||
|
||||
fun clearDirty(eTag: String)
|
||||
fun prepareForUpload(fileName: String, uid: String)
|
||||
|
||||
fun clearDirty(eTag: String?)
|
||||
|
||||
fun resetDeleted()
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ class LocalTask : AndroidTask, LocalResource<Task> {
|
||||
|
||||
private var saveAsDirty = false // When true, the resource will be saved as dirty
|
||||
|
||||
private var fileName: String? = null
|
||||
override var fileName: String? = null
|
||||
var eTag: String? = null
|
||||
|
||||
override val content: String
|
||||
@ -49,7 +49,7 @@ class LocalTask : AndroidTask, LocalResource<Task> {
|
||||
|
||||
override// Now the same
|
||||
val uuid: String?
|
||||
get() = fileName
|
||||
get() = task?.uid
|
||||
|
||||
constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?)
|
||||
: super(taskList, task) {
|
||||
@ -96,7 +96,7 @@ class LocalTask : AndroidTask, LocalResource<Task> {
|
||||
|
||||
/* custom queries */
|
||||
|
||||
override fun prepareForUpload() {
|
||||
override fun legacyPrepareForUpload(fileName_: String?) {
|
||||
var uid: String? = null
|
||||
val c = taskList.provider.client.query(taskSyncURI(), arrayOf(COLUMN_UID), null, null, null)
|
||||
if (c.moveToNext())
|
||||
@ -106,27 +106,40 @@ class LocalTask : AndroidTask, LocalResource<Task> {
|
||||
|
||||
c.close()
|
||||
|
||||
val fileName = fileName_ ?: uid
|
||||
val values = ContentValues(2)
|
||||
values.put(TaskContract.Tasks._SYNC_ID, uid)
|
||||
values.put(TaskContract.Tasks._SYNC_ID, fileName)
|
||||
values.put(COLUMN_UID, uid)
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null)
|
||||
|
||||
fileName = uid
|
||||
this.fileName = fileName
|
||||
val task = this.task
|
||||
if (task != null)
|
||||
task.uid = uid
|
||||
}
|
||||
|
||||
override fun prepareForUpload(fileName: String, uid: String) {
|
||||
val values = ContentValues(2)
|
||||
values.put(TaskContract.Tasks._SYNC_ID, fileName)
|
||||
values.put(COLUMN_UID, uid)
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null)
|
||||
|
||||
task?.uid = uid
|
||||
this.fileName = fileName
|
||||
}
|
||||
|
||||
override fun resetDeleted() {
|
||||
val values = ContentValues(1)
|
||||
values.put(TaskContract.Tasks._DELETED, 0)
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null)
|
||||
}
|
||||
|
||||
override fun clearDirty(eTag: String) {
|
||||
override fun clearDirty(eTag: String?) {
|
||||
val values = ContentValues(2)
|
||||
values.put(TaskContract.Tasks._DIRTY, 0)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
if (eTag != null) {
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
}
|
||||
if (task != null)
|
||||
values.put(COLUMN_SEQUENCE, task?.sequence)
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null)
|
||||
|
@ -19,6 +19,7 @@ import at.bitfire.ical4android.AndroidTaskListFactory
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import at.bitfire.ical4android.TaskProvider.ProviderName
|
||||
import com.etesync.syncadapter.CachedCollection
|
||||
import com.etesync.syncadapter.model.JournalEntity
|
||||
import org.dmfs.tasks.contract.TaskContract.TaskLists
|
||||
import org.dmfs.tasks.contract.TaskContract.Tasks
|
||||
@ -54,6 +55,14 @@ class LocalTaskList private constructor(
|
||||
return create(account, provider, values)
|
||||
}
|
||||
|
||||
fun create(account: Account, provider: TaskProvider, cachedCollection: CachedCollection): Uri {
|
||||
val values = valuesFromCachedCollection(cachedCollection, true)
|
||||
values.put(TaskLists.OWNER, account.name)
|
||||
values.put(TaskLists.SYNC_ENABLED, 1)
|
||||
values.put(TaskLists.VISIBLE, 1)
|
||||
return create(account, provider, values)
|
||||
}
|
||||
|
||||
fun findByName(account: Account, provider: TaskProvider, factory: Factory, name: String): LocalTaskList?
|
||||
= AndroidTaskList.find(account, provider, factory, TaskLists._SYNC_ID + "==?", arrayOf(name)).firstOrNull()
|
||||
|
||||
@ -70,6 +79,18 @@ class LocalTaskList private constructor(
|
||||
return values
|
||||
}
|
||||
|
||||
private fun valuesFromCachedCollection(cachedCollection: CachedCollection, withColor: Boolean): ContentValues {
|
||||
val col = cachedCollection.col
|
||||
val meta = cachedCollection.meta
|
||||
val values = ContentValues(3)
|
||||
values.put(TaskLists._SYNC_ID, col.uid)
|
||||
values.put(TaskLists.LIST_NAME, meta.name)
|
||||
|
||||
if (withColor)
|
||||
values.put(TaskLists.LIST_COLOR, LocalCalendar.parseColor(meta.color))
|
||||
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
override val url: String?
|
||||
@ -78,6 +99,9 @@ class LocalTaskList private constructor(
|
||||
fun update(journalEntity: JournalEntity, updateColor: Boolean) =
|
||||
update(valuesFromCollectionInfo(journalEntity, updateColor))
|
||||
|
||||
fun update(cachedCollection: CachedCollection, updateColor: Boolean) =
|
||||
update(valuesFromCachedCollection(cachedCollection, updateColor))
|
||||
|
||||
override fun findDeleted() = queryTasks("${Tasks._DELETED}!=0", null)
|
||||
|
||||
override fun findDirty(limit: Int?): List<LocalTask> {
|
||||
@ -101,7 +125,10 @@ class LocalTaskList private constructor(
|
||||
= queryTasks(Tasks._SYNC_ID + " IS NULL", null)
|
||||
|
||||
override fun findByUid(uid: String): LocalTask?
|
||||
= queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull()
|
||||
= queryTasks(LocalTask.COLUMN_UID + " =? ", arrayOf(uid)).firstOrNull()
|
||||
|
||||
override fun findByFilename(filename: String): LocalTask?
|
||||
= queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(filename)).firstOrNull()
|
||||
|
||||
override fun count(): Long {
|
||||
try {
|
||||
|
@ -15,10 +15,7 @@ import android.content.*
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import com.etesync.syncadapter.AccountSettings
|
||||
import com.etesync.syncadapter.App
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.*
|
||||
import com.etesync.syncadapter.log.Logger
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
import com.etesync.syncadapter.model.JournalEntity
|
||||
@ -36,9 +33,6 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
|
||||
|
||||
|
||||
private class AddressBooksSyncAdapter(context: Context) : SyncAdapterService.SyncAdapter(context) {
|
||||
override val syncErrorTitle = R.string.sync_error_contacts
|
||||
override val notificationManager = SyncNotification(context, "journals-contacts", Constants.NOTIFICATION_CONTACTS_SYNC)
|
||||
|
||||
override fun onPerformSyncDo(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
|
||||
if (contactsProvider == null) {
|
||||
@ -53,7 +47,11 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
|
||||
|
||||
RefreshCollections(account, CollectionInfo.Type.ADDRESS_BOOK).run()
|
||||
|
||||
updateLocalAddressBooks(contactsProvider, account)
|
||||
if (settings.isLegacy) {
|
||||
legacyUpdateLocalAddressBooks(contactsProvider, account)
|
||||
} else {
|
||||
updateLocalAddressBooks(contactsProvider, account, settings)
|
||||
}
|
||||
|
||||
contactsProvider.release()
|
||||
|
||||
@ -63,15 +61,57 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
|
||||
val syncExtras = Bundle(extras)
|
||||
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true)
|
||||
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true)
|
||||
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue)
|
||||
ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras)
|
||||
}
|
||||
|
||||
Logger.log.info("Address book sync complete")
|
||||
}
|
||||
|
||||
private fun updateLocalAddressBooks(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
|
||||
val remote = HashMap<String, CachedCollection>()
|
||||
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
|
||||
val collections: List<CachedCollection>
|
||||
synchronized(etebaseLocalCache) {
|
||||
val httpClient = HttpClient.Builder(context, settings).setForeground(false).build()
|
||||
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
|
||||
val colMgr = etebase.collectionManager
|
||||
|
||||
collections = etebaseLocalCache.collectionList(colMgr).filter { it.meta.collectionType == Constants.ETEBASE_TYPE_ADDRESS_BOOK }
|
||||
}
|
||||
|
||||
for (collection in collections) {
|
||||
remote[collection.col.uid] = collection
|
||||
}
|
||||
|
||||
val local = LocalAddressBook.find(context, provider, account)
|
||||
|
||||
// delete obsolete local calendar
|
||||
for (addressBook in local) {
|
||||
val url = addressBook.url
|
||||
val collection = remote[url]
|
||||
if (collection == null) {
|
||||
Logger.log.fine("Deleting obsolete local addressBook $url")
|
||||
addressBook.delete()
|
||||
} else {
|
||||
// remote CollectionInfo found for this local collection, update data
|
||||
Logger.log.fine("Updating local addressBook $url")
|
||||
addressBook.update(collection)
|
||||
// we already have a local addressBook for this remote collection, don't take into consideration anymore
|
||||
remote.remove(url)
|
||||
}
|
||||
}
|
||||
|
||||
// create new local calendars
|
||||
for (url in remote.keys) {
|
||||
val cachedCollection = remote[url]!!
|
||||
Logger.log.info("Adding local calendar list $cachedCollection")
|
||||
LocalAddressBook.create(context, provider, account, cachedCollection)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class, AuthenticatorException::class, OperationCanceledException::class, IOException::class)
|
||||
private fun updateLocalAddressBooks(provider: ContentProviderClient, account: Account) {
|
||||
private fun legacyUpdateLocalAddressBooks(provider: ContentProviderClient, account: Account) {
|
||||
val context = context
|
||||
val data = (getContext().applicationContext as App).data
|
||||
val service = JournalModel.Service.fetchOrCreate(data, account.name, CollectionInfo.Type.ADDRESS_BOOK)
|
||||
|
@ -17,6 +17,7 @@ import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.ical4android.Event
|
||||
import at.bitfire.ical4android.InvalidCalendarException
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import com.etebase.client.Item
|
||||
import com.etesync.syncadapter.AccountSettings
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
@ -43,7 +44,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
get() = context.getString(R.string.sync_error_calendar, account.name)
|
||||
|
||||
override val syncSuccessfullyTitle: String
|
||||
get() = context.getString(R.string.sync_successfully_calendar, info.displayName,
|
||||
get() = context.getString(R.string.sync_successfully_calendar, localCalendar().displayName,
|
||||
account.name)
|
||||
|
||||
init {
|
||||
@ -59,7 +60,9 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
if (!super.prepare())
|
||||
return false
|
||||
|
||||
journal = JournalEntryManager(httpClient.okHttpClient, remote, localCalendar().name!!)
|
||||
if (isLegacy) {
|
||||
journal = JournalEntryManager(httpClient.okHttpClient, remote, localCalendar().name!!)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -77,6 +80,32 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
return localCollection as LocalCalendar
|
||||
}
|
||||
|
||||
override fun processItem(item: Item) {
|
||||
val local = localCollection!!.findByFilename(item.uid)
|
||||
|
||||
if (!item.isDeleted) {
|
||||
val inputReader = StringReader(String(item.content))
|
||||
|
||||
val events = Event.eventsFromReader(inputReader)
|
||||
if (events.size == 0) {
|
||||
Logger.log.warning("Received VCard without data, ignoring")
|
||||
return
|
||||
} else if (events.size > 1) {
|
||||
Logger.log.warning("Received multiple VCALs, using first one")
|
||||
}
|
||||
|
||||
val event = events[0]
|
||||
processEvent(item, event, local)
|
||||
} else {
|
||||
if (local != null) {
|
||||
Logger.log.info("Removing local record #" + local.id + " which has been deleted on the server")
|
||||
local.delete()
|
||||
} else {
|
||||
Logger.log.warning("Tried deleting a non-existent record: " + item.uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class)
|
||||
override fun processSyncEntryImpl(cEntry: SyncEntry) {
|
||||
val inputReader = StringReader(cEntry.content)
|
||||
@ -93,7 +122,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
val local = localCollection!!.findByUid(event.uid!!)
|
||||
|
||||
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
|
||||
processEvent(event, local)
|
||||
legacyProcessEvent(event, local)
|
||||
} else {
|
||||
if (local != null) {
|
||||
Logger.log.info("Removing local record #" + local.id + " which has been deleted on the server")
|
||||
@ -105,8 +134,8 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class, IOException::class)
|
||||
override fun createLocalEntries() {
|
||||
super.createLocalEntries()
|
||||
override fun prepareLocal() {
|
||||
super.prepareLocal()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
createInviteAttendeesNotification()
|
||||
@ -128,7 +157,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
private fun createInviteAttendeesNotification(event: Event, icsContent: String) {
|
||||
val intent = EventEmailInvitation(context, account).createIntent(event, icsContent)
|
||||
if (intent != null) {
|
||||
val notificationHelper = SyncNotification(context, event.uid!!, event.uid!!.hashCode())
|
||||
val notificationHelper = SyncNotification(context, icsContent, event.hashCode())
|
||||
notificationHelper.notify(
|
||||
context.getString(
|
||||
R.string.sync_calendar_attendees_notification_title, event.summary),
|
||||
@ -138,8 +167,26 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
}
|
||||
}
|
||||
|
||||
private fun processEvent(item: Item, newData: Event, _localEvent: LocalEvent?): LocalEvent {
|
||||
var localEvent = _localEvent
|
||||
// delete local event, if it exists
|
||||
if (localEvent != null) {
|
||||
Logger.log.info("Updating " + newData.uid + " in local calendar")
|
||||
localEvent.eTag = item.etag
|
||||
localEvent.update(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
} else {
|
||||
Logger.log.info("Adding " + newData.uid + " to local calendar")
|
||||
localEvent = LocalEvent(localCalendar(), newData, item.uid, item.etag)
|
||||
localEvent.add()
|
||||
syncResult.stats.numInserts++
|
||||
}
|
||||
|
||||
return localEvent
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class)
|
||||
private fun processEvent(newData: Event, _localEvent: LocalEvent?): LocalEvent {
|
||||
private fun legacyProcessEvent(newData: Event, _localEvent: LocalEvent?): LocalEvent {
|
||||
var localEvent = _localEvent
|
||||
// delete local event, if it exists
|
||||
if (localEvent != null) {
|
||||
|
@ -13,10 +13,7 @@ import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import com.etesync.syncadapter.AccountSettings
|
||||
import com.etesync.syncadapter.App
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.*
|
||||
import com.etesync.syncadapter.log.Logger
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
import com.etesync.syncadapter.model.JournalEntity
|
||||
@ -32,9 +29,6 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
|
||||
|
||||
|
||||
private class SyncAdapter(context: Context) : SyncAdapterService.SyncAdapter(context) {
|
||||
override val syncErrorTitle = R.string.sync_error_calendar
|
||||
override val notificationManager = SyncNotification(context, "journals-calendar", Constants.NOTIFICATION_CALENDAR_SYNC)
|
||||
|
||||
override fun onPerformSyncDo(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
val settings = AccountSettings(context, account)
|
||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
|
||||
@ -42,7 +36,11 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
|
||||
|
||||
RefreshCollections(account, CollectionInfo.Type.CALENDAR).run()
|
||||
|
||||
updateLocalCalendars(provider, account, settings)
|
||||
if (settings.isLegacy) {
|
||||
legacyUpdateLocalCalendars(provider, account, settings)
|
||||
} else {
|
||||
updateLocalCalendars(provider, account, settings)
|
||||
}
|
||||
|
||||
val principal = settings.uri?.toHttpUrlOrNull()!!
|
||||
|
||||
@ -56,8 +54,52 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
|
||||
Logger.log.info("Calendar sync complete")
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
private fun updateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
|
||||
val remote = HashMap<String, CachedCollection>()
|
||||
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
|
||||
val collections: List<CachedCollection>
|
||||
synchronized(etebaseLocalCache) {
|
||||
val httpClient = HttpClient.Builder(context, settings).setForeground(false).build()
|
||||
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
|
||||
val colMgr = etebase.collectionManager
|
||||
|
||||
collections = etebaseLocalCache.collectionList(colMgr).filter { it.meta.collectionType == Constants.ETEBASE_TYPE_CALENDAR }
|
||||
}
|
||||
|
||||
for (collection in collections) {
|
||||
remote[collection.col.uid] = collection
|
||||
}
|
||||
|
||||
val local = AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null)
|
||||
|
||||
val updateColors = settings.manageCalendarColors
|
||||
|
||||
// delete obsolete local calendar
|
||||
for (calendar in local) {
|
||||
val url = calendar.name
|
||||
val collection = remote[url]
|
||||
if (collection == null) {
|
||||
Logger.log.fine("Deleting obsolete local calendar $url")
|
||||
calendar.delete()
|
||||
} else {
|
||||
// remote CollectionInfo found for this local collection, update data
|
||||
Logger.log.fine("Updating local calendar $url")
|
||||
calendar.update(collection, updateColors)
|
||||
// we already have a local calendar for this remote collection, don't take into consideration anymore
|
||||
remote.remove(url)
|
||||
}
|
||||
}
|
||||
|
||||
// create new local calendars
|
||||
for (url in remote.keys) {
|
||||
val cachedCollection = remote[url]!!
|
||||
Logger.log.info("Adding local calendar list $cachedCollection")
|
||||
LocalCalendar.create(account, provider, cachedCollection)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
private fun legacyUpdateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
|
||||
val data = (context.applicationContext as App).data
|
||||
val service = JournalModel.Service.fetchOrCreate(data, account.name, CollectionInfo.Type.CALENDAR)
|
||||
|
||||
|
@ -27,9 +27,6 @@ class ContactsSyncAdapterService : SyncAdapterService() {
|
||||
|
||||
|
||||
private class ContactsSyncAdapter(context: Context) : SyncAdapterService.SyncAdapter(context) {
|
||||
override val syncErrorTitle = R.string.sync_error_contacts
|
||||
override val notificationManager = SyncNotification(context, "journals-contacts", Constants.NOTIFICATION_CONTACTS_SYNC)
|
||||
|
||||
override fun onPerformSyncDo(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
val addressBook = LocalAddressBook(context, account, provider)
|
||||
|
||||
|
@ -19,6 +19,7 @@ import at.bitfire.vcard4android.ContactsStorageException
|
||||
import com.etesync.journalmanager.Exceptions
|
||||
import com.etesync.journalmanager.JournalEntryManager
|
||||
import com.etesync.journalmanager.model.SyncEntry
|
||||
import com.etebase.client.Item
|
||||
import com.etesync.syncadapter.AccountSettings
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.HttpClient
|
||||
@ -77,7 +78,9 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
}
|
||||
}
|
||||
|
||||
journal = JournalEntryManager(httpClient.okHttpClient, remote, localAddressBook.url)
|
||||
if (isLegacy) {
|
||||
journal = JournalEntryManager(httpClient.okHttpClient, remote, localAddressBook.url)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -127,6 +130,32 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
return localCollection as LocalAddressBook
|
||||
}
|
||||
|
||||
override fun processItem(item: Item) {
|
||||
val local = localCollection!!.findByFilename(item.uid)
|
||||
|
||||
if (!item.isDeleted) {
|
||||
val inputReader = StringReader(String(item.content))
|
||||
|
||||
val contacts = Contact.fromReader(inputReader, resourceDownloader)
|
||||
if (contacts.size == 0) {
|
||||
Logger.log.warning("Received VCard without data, ignoring")
|
||||
return
|
||||
} else if (contacts.size > 1) {
|
||||
Logger.log.warning("Received multiple VCALs, using first one")
|
||||
}
|
||||
|
||||
val contact = contacts[0]
|
||||
processContact(item, contact, local)
|
||||
} else {
|
||||
if (local != null) {
|
||||
Logger.log.info("Removing local record which has been deleted on the server")
|
||||
local.delete()
|
||||
} else {
|
||||
Logger.log.warning("Tried deleting a non-existent record: " + item.uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class)
|
||||
override fun processSyncEntryImpl(cEntry: SyncEntry) {
|
||||
val inputReader = StringReader(cEntry.content)
|
||||
@ -142,7 +171,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
val local = localCollection!!.findByUid(contact.uid!!)
|
||||
|
||||
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
|
||||
processContact(contact, local)
|
||||
legacyProcessContact(contact, local)
|
||||
} else {
|
||||
if (local != null) {
|
||||
Logger.log.info("Removing local record which has been deleted on the server")
|
||||
@ -153,8 +182,65 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
}
|
||||
}
|
||||
|
||||
private fun processContact(item: Item, newData: Contact, _local: LocalAddress?): LocalAddress {
|
||||
var local = _local
|
||||
val uuid = newData.uid
|
||||
// update local contact, if it exists
|
||||
if (local != null) {
|
||||
Logger.log.log(Level.INFO, "Updating $uuid in local address book")
|
||||
|
||||
if (local is LocalGroup && newData.group) {
|
||||
// update group
|
||||
val group: LocalGroup = local
|
||||
group.eTag = item.etag
|
||||
group.update(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
|
||||
} else if (local is LocalContact && !newData.group) {
|
||||
// update contact
|
||||
val contact: LocalContact = local
|
||||
contact.eTag = item.etag
|
||||
contact.update(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
|
||||
} else {
|
||||
// group has become an individual contact or vice versa
|
||||
try {
|
||||
local.delete()
|
||||
local = null
|
||||
} catch (e: CalendarStorageException) {
|
||||
// CalendarStorageException is not used by LocalGroup and LocalContact
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (local == null) {
|
||||
if (newData.group) {
|
||||
Logger.log.log(Level.INFO, "Creating local group", item.uid)
|
||||
val group = LocalGroup(localAddressBook(), newData, item.uid, item.etag)
|
||||
group.add()
|
||||
|
||||
local = group
|
||||
} else {
|
||||
Logger.log.log(Level.INFO, "Creating local contact", item.uid)
|
||||
val contact = LocalContact(localAddressBook(), newData, item.uid, item.etag)
|
||||
contact.add()
|
||||
|
||||
local = contact
|
||||
}
|
||||
syncResult.stats.numInserts++
|
||||
}
|
||||
|
||||
if (LocalContact.HASH_HACK && local is LocalContact)
|
||||
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
||||
local.updateHashCode(null)
|
||||
|
||||
return local
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ContactsStorageException::class)
|
||||
private fun processContact(newData: Contact, _local: LocalAddress?): LocalAddress {
|
||||
private fun legacyProcessContact(newData: Contact, _local: LocalAddress?): LocalAddress {
|
||||
var local = _local
|
||||
val uuid = newData.uid
|
||||
// update local contact, if it exists
|
||||
|
@ -22,11 +22,14 @@ import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.util.Pair
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import com.etebase.client.FetchOptions
|
||||
import com.etebase.client.exceptions.ConnectionException
|
||||
import com.etebase.client.exceptions.TemporaryServerErrorException
|
||||
import com.etebase.client.exceptions.UnauthorizedException
|
||||
import com.etesync.syncadapter.*
|
||||
import com.etesync.journalmanager.Crypto
|
||||
import com.etesync.journalmanager.Exceptions
|
||||
import com.etesync.journalmanager.JournalManager
|
||||
import com.etesync.syncadapter.*
|
||||
import com.etesync.syncadapter.log.Logger
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
import com.etesync.syncadapter.model.JournalEntity
|
||||
@ -96,8 +99,8 @@ abstract class SyncAdapterService : Service() {
|
||||
}
|
||||
|
||||
abstract class SyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, false) {
|
||||
abstract val syncErrorTitle: Int
|
||||
abstract val notificationManager: SyncNotification
|
||||
private val syncErrorTitle: Int = R.string.sync_error_generic
|
||||
private val notificationManager = SyncNotification(context, "refresh-collections", Constants.NOTIFICATION_REFRESH_COLLECTIONS)
|
||||
|
||||
abstract fun onPerformSyncDo(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult)
|
||||
|
||||
@ -117,6 +120,12 @@ abstract class SyncAdapterService : Service() {
|
||||
} catch (e: Exceptions.ServiceUnavailableException) {
|
||||
syncResult.stats.numIoExceptions++
|
||||
syncResult.delayUntil = if (e.retryAfter > 0) e.retryAfter else Constants.DEFAULT_RETRY_DELAY
|
||||
} catch (e: TemporaryServerErrorException) {
|
||||
syncResult.stats.numIoExceptions++
|
||||
syncResult.delayUntil = Constants.DEFAULT_RETRY_DELAY
|
||||
} catch (e: ConnectionException) {
|
||||
syncResult.stats.numIoExceptions++
|
||||
syncResult.delayUntil = Constants.DEFAULT_RETRY_DELAY
|
||||
} catch (e: Exceptions.IgnorableHttpException) {
|
||||
// Ignore
|
||||
} catch (e: Exception) {
|
||||
@ -132,7 +141,7 @@ abstract class SyncAdapterService : Service() {
|
||||
|
||||
val detailsIntent = notificationManager.detailsIntent
|
||||
detailsIntent.putExtra(Constants.KEY_ACCOUNT, account)
|
||||
if (e !is Exceptions.UnauthorizedException) {
|
||||
if (e !is Exceptions.UnauthorizedException && e !is UnauthorizedException) {
|
||||
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority)
|
||||
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase)
|
||||
}
|
||||
@ -208,30 +217,70 @@ abstract class SyncAdapterService : Service() {
|
||||
val settings = AccountSettings(context, account)
|
||||
val httpClient = HttpClient.Builder(context, settings).setForeground(false).build()
|
||||
|
||||
val journalsManager = JournalManager(httpClient.okHttpClient, settings.uri?.toHttpUrlOrNull()!!)
|
||||
if (settings.isLegacy) {
|
||||
val journalsManager = JournalManager(httpClient.okHttpClient, settings.uri?.toHttpUrlOrNull()!!)
|
||||
|
||||
var journals = journalFetcher.list(journalsManager, settings, serviceType)
|
||||
var journals = journalFetcher.list(journalsManager, settings, serviceType)
|
||||
|
||||
if (journals.isEmpty()) {
|
||||
journals = LinkedList()
|
||||
try {
|
||||
val info = CollectionInfo.defaultForServiceType(serviceType)
|
||||
val uid = JournalManager.Journal.genUid()
|
||||
info.uid = uid
|
||||
val crypto = Crypto.CryptoManager(info.version, settings.password(), uid)
|
||||
val journal = JournalManager.Journal(crypto, info.toJson(), uid)
|
||||
journalsManager.create(journal)
|
||||
journals.add(Pair(journal, info))
|
||||
} catch (e: Exceptions.AssociateNotAllowedException) {
|
||||
// Skip for now
|
||||
if (journals.isEmpty()) {
|
||||
journals = LinkedList()
|
||||
try {
|
||||
val info = CollectionInfo.defaultForServiceType(serviceType)
|
||||
val uid = JournalManager.Journal.genUid()
|
||||
info.uid = uid
|
||||
val crypto = Crypto.CryptoManager(info.version, settings.password(), uid)
|
||||
val journal = JournalManager.Journal(crypto, info.toJson(), uid)
|
||||
journalsManager.create(journal)
|
||||
journals.add(Pair(journal, info))
|
||||
} catch (e: Exceptions.AssociateNotAllowedException) {
|
||||
// Skip for now
|
||||
}
|
||||
}
|
||||
|
||||
legacySaveCollections(journals)
|
||||
|
||||
httpClient.close()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
|
||||
synchronized(etebaseLocalCache) {
|
||||
val cacheAge = 5 * 1000 // 5 seconds - it's just a hack for burst fetching
|
||||
val now = System.currentTimeMillis()
|
||||
val lastCollectionsFetch = collectionLastFetchMap[account.name] ?: 0
|
||||
|
||||
if (abs(now - lastCollectionsFetch) <= cacheAge) {
|
||||
return@synchronized
|
||||
}
|
||||
|
||||
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
|
||||
val colMgr = etebase.collectionManager
|
||||
var stoken = etebaseLocalCache.loadStoken()
|
||||
var done = false
|
||||
while (!done) {
|
||||
val colList = colMgr.list(FetchOptions().stoken(stoken))
|
||||
for (col in colList.data) {
|
||||
etebaseLocalCache.collectionSet(colMgr, col)
|
||||
}
|
||||
|
||||
for (col in colList.removedMemberships) {
|
||||
etebaseLocalCache.collectionUnset(colMgr, col.uid())
|
||||
}
|
||||
|
||||
stoken = colList.stoken
|
||||
done = colList.isDone
|
||||
if (stoken != null) {
|
||||
etebaseLocalCache.saveStoken(stoken)
|
||||
}
|
||||
}
|
||||
collectionLastFetchMap[account.name] = now
|
||||
}
|
||||
|
||||
saveCollections(journals)
|
||||
httpClient.close()
|
||||
}
|
||||
|
||||
private fun saveCollections(journals: Iterable<Pair<JournalManager.Journal, CollectionInfo>>) {
|
||||
private fun legacySaveCollections(journals: Iterable<Pair<JournalManager.Journal, CollectionInfo>>) {
|
||||
val data = (context.applicationContext as App).data
|
||||
val service = JournalModel.Service.fetchOrCreate(data, account.name, serviceType)
|
||||
|
||||
@ -269,5 +318,6 @@ abstract class SyncAdapterService : Service() {
|
||||
|
||||
companion object {
|
||||
val journalFetcher = CachedJournalFetcher()
|
||||
var collectionLastFetchMap = HashMap<String, Long>()
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,11 @@ import android.os.Bundle
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.ical4android.InvalidCalendarException
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import com.etebase.client.*
|
||||
import com.etebase.client.exceptions.ConnectionException
|
||||
import com.etebase.client.exceptions.HttpException
|
||||
import com.etebase.client.exceptions.TemporaryServerErrorException
|
||||
import com.etebase.client.exceptions.UnauthorizedException
|
||||
import com.etesync.syncadapter.*
|
||||
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
|
||||
import com.etesync.journalmanager.Crypto
|
||||
@ -25,10 +30,13 @@ import com.etesync.journalmanager.model.SyncEntry
|
||||
import com.etesync.syncadapter.log.Logger
|
||||
import com.etesync.syncadapter.model.*
|
||||
import com.etesync.journalmanager.model.SyncEntry.Actions.ADD
|
||||
import com.etesync.syncadapter.HttpClient
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.resource.*
|
||||
import com.etesync.syncadapter.ui.AccountsActivity
|
||||
import com.etesync.syncadapter.ui.DebugInfoActivity
|
||||
import com.etesync.syncadapter.ui.ViewCollectionActivity
|
||||
import com.etesync.syncadapter.ui.etebase.CollectionActivity
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
import java.io.Closeable
|
||||
import java.io.FileNotFoundException
|
||||
@ -41,21 +49,35 @@ import kotlin.concurrent.withLock
|
||||
|
||||
abstract class SyncManager<T: LocalResource<*>> @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
|
||||
constructor(protected val context: Context, protected val account: Account, protected val settings: AccountSettings, protected val extras: Bundle, protected val authority: String, protected val syncResult: SyncResult, journalUid: String, protected val serviceType: CollectionInfo.Type, accountName: String): Closeable {
|
||||
// FIXME: remove all of the lateinit once we remove legacy (and make immutable)
|
||||
// RemoteEntries and the likes are probably also just relevant for legacy
|
||||
protected val isLegacy: Boolean = settings.isLegacy
|
||||
|
||||
protected val notificationManager: SyncNotification
|
||||
protected val info: CollectionInfo
|
||||
protected lateinit var info: CollectionInfo
|
||||
protected var localCollection: LocalCollection<T>? = null
|
||||
|
||||
protected var httpClient: HttpClient
|
||||
|
||||
protected lateinit var etebaseLocalCache: EtebaseLocalCache
|
||||
protected lateinit var etebase: com.etebase.client.Account
|
||||
protected lateinit var colMgr: CollectionManager
|
||||
protected lateinit var itemMgr: ItemManager
|
||||
protected lateinit var cachedCollection: CachedCollection
|
||||
|
||||
// Sync counters
|
||||
private var syncItemsTotal = 0
|
||||
private var syncItemsDeleted = 0
|
||||
private var syncItemsChanged = 0
|
||||
|
||||
protected var journal: JournalEntryManager? = null
|
||||
private var _journalEntity: JournalEntity? = null
|
||||
|
||||
private var numDiscarded = 0
|
||||
|
||||
private val crypto: Crypto.CryptoManager
|
||||
private lateinit var crypto: Crypto.CryptoManager
|
||||
|
||||
private val data: MyEntityDataStore
|
||||
private lateinit var data: MyEntityDataStore
|
||||
|
||||
/**
|
||||
* remote CTag (uuid of the last entry on the server). We update it when we fetch/push and save when everything works.
|
||||
@ -89,21 +111,31 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
// create HttpClient with given logger
|
||||
httpClient = HttpClient.Builder(context, settings).setForeground(false).build()
|
||||
|
||||
data = (context.applicationContext as App).data
|
||||
val serviceEntity = JournalModel.Service.fetchOrCreate(data, accountName, serviceType)
|
||||
info = JournalEntity.fetch(data, serviceEntity, journalUid)!!.info
|
||||
if (isLegacy) {
|
||||
data = (context.applicationContext as App).data
|
||||
val serviceEntity = JournalModel.Service.fetchOrCreate(data, accountName, serviceType)
|
||||
info = JournalEntity.fetch(data, serviceEntity, journalUid)!!.info
|
||||
|
||||
Logger.log.info(String.format(Locale.getDefault(), "Syncing collection %s (version: %d)", journalUid, info.version))
|
||||
|
||||
if (journalEntity.encryptedKey != null) {
|
||||
crypto = Crypto.CryptoManager(info.version, settings.keyPair!!, journalEntity.encryptedKey)
|
||||
} else {
|
||||
crypto = Crypto.CryptoManager(info.version, settings.password(), info.uid!!)
|
||||
}
|
||||
} else {
|
||||
etebaseLocalCache = EtebaseLocalCache.getInstance(context, accountName)
|
||||
etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
|
||||
colMgr = etebase.collectionManager
|
||||
synchronized(etebaseLocalCache) {
|
||||
cachedCollection = etebaseLocalCache.collectionGet(colMgr, journalUid)!!
|
||||
}
|
||||
itemMgr = colMgr.getItemManager(cachedCollection.col)
|
||||
}
|
||||
|
||||
// dismiss previous error notifications
|
||||
notificationManager = SyncNotification(context, journalUid, notificationId())
|
||||
notificationManager.cancel()
|
||||
|
||||
Logger.log.info(String.format(Locale.getDefault(), "Syncing collection %s (version: %d)", journalUid, info.version))
|
||||
|
||||
if (journalEntity.encryptedKey != null) {
|
||||
crypto = Crypto.CryptoManager(info.version, settings.keyPair!!, journalEntity.encryptedKey)
|
||||
} else {
|
||||
crypto = Crypto.CryptoManager(info.version, settings.password(), info.uid!!)
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun notificationId(): Int
|
||||
@ -114,6 +146,10 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
|
||||
@TargetApi(21)
|
||||
fun performSync() {
|
||||
syncItemsTotal = 0
|
||||
syncItemsDeleted = 0
|
||||
syncItemsChanged = 0
|
||||
|
||||
var syncPhase = R.string.sync_phase_prepare
|
||||
try {
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
@ -128,48 +164,101 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
prepareFetch()
|
||||
|
||||
do {
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_fetch_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
fetchEntries()
|
||||
if (isLegacy) {
|
||||
do {
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_fetch_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
fetchEntries()
|
||||
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_apply_remote_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
applyRemoteEntries()
|
||||
} while (remoteEntries!!.size == MAX_FETCH)
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_apply_remote_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
applyRemoteEntries()
|
||||
} while (remoteEntries!!.size == MAX_FETCH)
|
||||
|
||||
do {
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_prepare_local
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
prepareLocal()
|
||||
do {
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_prepare_local
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
prepareLocal()
|
||||
|
||||
/* Create journal entries out of local changes. */
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_create_local_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
createLocalEntries()
|
||||
/* Create journal entries out of local changes. */
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_create_local_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
createLocalEntries()
|
||||
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_apply_local_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
/* FIXME: Skipping this now, because we already override with remote.
|
||||
applyLocalEntries();
|
||||
*/
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_apply_local_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
/* FIXME: Skipping this now, because we already override with remote.
|
||||
applyLocalEntries();
|
||||
*/
|
||||
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_push_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
pushEntries()
|
||||
} while (localEntries!!.size == MAX_PUSH)
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_push_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
pushEntries()
|
||||
} while (localEntries!!.size == MAX_PUSH)
|
||||
} else {
|
||||
var itemList: ItemListResponse?
|
||||
var stoken = synchronized(etebaseLocalCache) {
|
||||
etebaseLocalCache.collectionLoadStoken(cachedCollection.col.uid)
|
||||
}
|
||||
// Push local changes
|
||||
var chunkPushItems: List<Item>
|
||||
do {
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_prepare_local
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
prepareLocal()
|
||||
|
||||
/* Create push items out of local changes. */
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_create_local_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
chunkPushItems = createPushItems()
|
||||
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_push_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
pushItems(chunkPushItems)
|
||||
} while (chunkPushItems.size == MAX_PUSH)
|
||||
|
||||
do {
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_fetch_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
itemList = fetchItems(stoken)
|
||||
if (itemList == null) {
|
||||
break
|
||||
}
|
||||
|
||||
if (Thread.interrupted())
|
||||
throw InterruptedException()
|
||||
syncPhase = R.string.sync_phase_apply_remote_entries
|
||||
Logger.log.info("Sync phase: " + context.getString(syncPhase))
|
||||
applyRemoteItems(itemList)
|
||||
|
||||
stoken = itemList.stoken
|
||||
if (stoken != null) {
|
||||
synchronized(etebaseLocalCache) {
|
||||
etebaseLocalCache.collectionSaveStoken(cachedCollection.col.uid, stoken)
|
||||
}
|
||||
}
|
||||
} while (!itemList!!.isDone)
|
||||
}
|
||||
|
||||
/* Cleanup and finalize changes */
|
||||
if (Thread.interrupted())
|
||||
@ -187,6 +276,11 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
} catch (e: SSLHandshakeException) {
|
||||
syncResult.stats.numIoExceptions++
|
||||
|
||||
notificationManager.setThrowable(e)
|
||||
val detailsIntent = notificationManager.detailsIntent
|
||||
detailsIntent.putExtra(KEY_ACCOUNT, account)
|
||||
notificationManager.notify(syncErrorTitle, context.getString(syncPhase))
|
||||
} catch (e: FileNotFoundException) {
|
||||
notificationManager.setThrowable(e)
|
||||
val detailsIntent = notificationManager.detailsIntent
|
||||
detailsIntent.putExtra(KEY_ACCOUNT, account)
|
||||
@ -197,15 +291,21 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
} catch (e: Exceptions.ServiceUnavailableException) {
|
||||
syncResult.stats.numIoExceptions++
|
||||
syncResult.delayUntil = if (e.retryAfter > 0) e.retryAfter else Constants.DEFAULT_RETRY_DELAY
|
||||
} catch (e: TemporaryServerErrorException) {
|
||||
syncResult.stats.numIoExceptions++
|
||||
syncResult.delayUntil = Constants.DEFAULT_RETRY_DELAY
|
||||
} catch (e: ConnectionException) {
|
||||
syncResult.stats.numIoExceptions++
|
||||
syncResult.delayUntil = Constants.DEFAULT_RETRY_DELAY
|
||||
} catch (e: InterruptedException) {
|
||||
// Restart sync if interrupted
|
||||
syncResult.fullSyncRequested = true
|
||||
} catch (e: Exceptions.IgnorableHttpException) {
|
||||
// Ignore
|
||||
} catch (e: Exception) {
|
||||
if (e is Exceptions.UnauthorizedException) {
|
||||
if (e is Exceptions.UnauthorizedException || e is UnauthorizedException) {
|
||||
syncResult.stats.numAuthExceptions++
|
||||
} else if (e is Exceptions.HttpException) {
|
||||
} else if (e is Exceptions.HttpException || e is HttpException) {
|
||||
syncResult.stats.numParseExceptions++
|
||||
} else if (e is CalendarStorageException || e is ContactsStorageException) {
|
||||
syncResult.databaseError = true
|
||||
@ -241,38 +341,28 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
|
||||
private fun notifyUserOnSync() {
|
||||
val changeNotification = context.defaultSharedPreferences.getBoolean(App.CHANGE_NOTIFICATION, true)
|
||||
if (remoteEntries!!.isEmpty() || !changeNotification) {
|
||||
|
||||
if (!changeNotification || (syncItemsTotal == 0)) {
|
||||
return
|
||||
}
|
||||
|
||||
val notificationHelper = SyncNotification(context,
|
||||
System.currentTimeMillis().toString(), notificationId())
|
||||
|
||||
var deleted = 0
|
||||
var added = 0
|
||||
var changed = 0
|
||||
for (entry in remoteEntries!!) {
|
||||
val cEntry = SyncEntry.fromJournalEntry(crypto, entry)
|
||||
val action = cEntry.action
|
||||
when (action) {
|
||||
ADD -> added++
|
||||
SyncEntry.Actions.DELETE -> deleted++
|
||||
SyncEntry.Actions.CHANGE -> changed++
|
||||
}
|
||||
}
|
||||
|
||||
val resources = context.resources
|
||||
val intent = ViewCollectionActivity.newIntent(context, account, info)
|
||||
val intent = if (isLegacy) {
|
||||
ViewCollectionActivity.newIntent(context, account, info)
|
||||
} else {
|
||||
CollectionActivity.newIntent(context, account, cachedCollection.col.uid)
|
||||
}
|
||||
notificationHelper.notify(syncSuccessfullyTitle,
|
||||
String.format(context.getString(R.string.sync_successfully_modified),
|
||||
resources.getQuantityString(R.plurals.sync_successfully,
|
||||
remoteEntries!!.size, remoteEntries!!.size)),
|
||||
syncItemsTotal, syncItemsTotal)),
|
||||
String.format(context.getString(R.string.sync_successfully_modified_full),
|
||||
resources.getQuantityString(R.plurals.sync_successfully,
|
||||
added, added),
|
||||
syncItemsChanged, syncItemsChanged),
|
||||
resources.getQuantityString(R.plurals.sync_successfully,
|
||||
changed, changed),
|
||||
resources.getQuantityString(R.plurals.sync_successfully,
|
||||
deleted, deleted)),
|
||||
syncItemsDeleted, syncItemsDeleted)),
|
||||
intent)
|
||||
}
|
||||
|
||||
@ -287,6 +377,25 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
return true
|
||||
}
|
||||
|
||||
protected abstract fun processItem(item: Item)
|
||||
|
||||
private fun persistItem(item: Item) {
|
||||
synchronized(etebaseLocalCache) {
|
||||
// FIXME: it's terrible that we are fetching and decrypting the item here - we really don't have to
|
||||
val cached = etebaseLocalCache.itemGet(itemMgr, cachedCollection.col.uid, item.uid)
|
||||
if (cached?.item?.etag != item.etag) {
|
||||
syncItemsTotal++
|
||||
|
||||
if (item.isDeleted) {
|
||||
syncItemsDeleted++
|
||||
} else {
|
||||
syncItemsChanged++
|
||||
}
|
||||
etebaseLocalCache.itemSet(itemMgr, cachedCollection.col.uid, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class)
|
||||
protected abstract fun processSyncEntryImpl(cEntry: SyncEntry)
|
||||
|
||||
@ -317,38 +426,54 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, Exceptions.HttpException::class, InvalidCalendarException::class, InterruptedException::class)
|
||||
protected fun applyLocalEntries() {
|
||||
// FIXME: Need a better strategy
|
||||
// We re-apply local entries so our changes override whatever was written in the remote.
|
||||
val strTotal = localEntries!!.size.toString()
|
||||
var i = 0
|
||||
|
||||
for (entry in localEntries!!) {
|
||||
if (Thread.interrupted()) {
|
||||
throw InterruptedException()
|
||||
}
|
||||
i++
|
||||
Logger.log.info("Processing (" + i.toString() + "/" + strTotal + ") " + entry.toString())
|
||||
|
||||
val cEntry = SyncEntry.fromJournalEntry(crypto, entry)
|
||||
if (cEntry.isAction(SyncEntry.Actions.DELETE)) {
|
||||
continue
|
||||
}
|
||||
Logger.log.info("Processing resource for journal entry")
|
||||
processSyncEntry(cEntry)
|
||||
when (syncEntry.action) {
|
||||
ADD -> syncItemsChanged++
|
||||
SyncEntry.Actions.DELETE -> syncItemsDeleted++
|
||||
SyncEntry.Actions.CHANGE -> syncItemsChanged++
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, CalendarStorageException::class, ContactsStorageException::class)
|
||||
protected fun prepareFetch() {
|
||||
remoteCTag = journalEntity.getLastUid(data)
|
||||
if (isLegacy) {
|
||||
remoteCTag = journalEntity.getLastUid(data)
|
||||
} else {
|
||||
remoteCTag = cachedCollection.col.stoken
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchItems(stoken: String?): ItemListResponse? {
|
||||
if (remoteCTag != stoken) {
|
||||
val ret = itemMgr.list(FetchOptions().stoken(stoken))
|
||||
Logger.log.info("Fetched items. Done=${ret.isDone}")
|
||||
return ret
|
||||
} else {
|
||||
Logger.log.info("Skipping fetch because local stoken == lastStoken (${remoteCTag})")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyRemoteItems(itemList: ItemListResponse) {
|
||||
val items = itemList.data
|
||||
// Process new vcards from server
|
||||
val size = items.size
|
||||
var i = 0
|
||||
|
||||
for (item in items) {
|
||||
if (Thread.interrupted()) {
|
||||
throw InterruptedException()
|
||||
}
|
||||
i++
|
||||
Logger.log.info("Processing (${i}/${size}) UID=${item.uid} Etag=${item.etag}")
|
||||
|
||||
processItem(item)
|
||||
persistItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exceptions.HttpException::class, ContactsStorageException::class, CalendarStorageException::class, Exceptions.IntegrityException::class)
|
||||
protected fun fetchEntries() {
|
||||
private fun fetchEntries() {
|
||||
val count = data.count(EntryEntity::class.java).where(EntryEntity.JOURNAL.eq(journalEntity)).get().value()
|
||||
if (remoteCTag != null && count == 0) {
|
||||
// If we are updating an existing installation with no saved journal, we need to add
|
||||
@ -377,11 +502,13 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class, InterruptedException::class)
|
||||
protected fun applyRemoteEntries() {
|
||||
private fun applyRemoteEntries() {
|
||||
// Process new vcards from server
|
||||
val strTotal = remoteEntries!!.size.toString()
|
||||
var i = 0
|
||||
|
||||
syncItemsTotal += remoteEntries!!.size
|
||||
|
||||
for (entry in remoteEntries!!) {
|
||||
if (Thread.interrupted()) {
|
||||
throw InterruptedException()
|
||||
@ -406,7 +533,7 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
}
|
||||
|
||||
@Throws(Exceptions.HttpException::class, IOException::class, ContactsStorageException::class, CalendarStorageException::class)
|
||||
protected fun pushEntries() {
|
||||
private fun pushEntries() {
|
||||
// upload dirty contacts
|
||||
var pushed = 0
|
||||
// FIXME: Deal with failure (someone else uploaded before we go here)
|
||||
@ -456,8 +583,128 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
}
|
||||
}
|
||||
|
||||
private fun pushItems(chunkPushItems_: List<Item>) {
|
||||
var chunkPushItems = chunkPushItems_
|
||||
// upload dirty contacts
|
||||
var pushed = 0
|
||||
try {
|
||||
if (!chunkPushItems.isEmpty()) {
|
||||
val items = chunkPushItems
|
||||
itemMgr.batch(items.toTypedArray())
|
||||
|
||||
// Persist the items
|
||||
synchronized(etebaseLocalCache) {
|
||||
val colUid = cachedCollection.col.uid
|
||||
|
||||
for (item in items) {
|
||||
etebaseLocalCache.itemSet(itemMgr, colUid, item)
|
||||
}
|
||||
}
|
||||
|
||||
pushed += items.size
|
||||
}
|
||||
} finally {
|
||||
// FIXME: A bit fragile, we assume the order in createPushItems
|
||||
var left = pushed
|
||||
for (local in localDeleted!!) {
|
||||
if (pushed-- <= 0) {
|
||||
break
|
||||
}
|
||||
local.delete()
|
||||
}
|
||||
if (left > 0) {
|
||||
localDeleted = localDeleted?.drop(left)
|
||||
chunkPushItems = chunkPushItems.drop(left - pushed)
|
||||
}
|
||||
|
||||
left = pushed
|
||||
var i = 0
|
||||
for (local in localDirty) {
|
||||
if (pushed-- <= 0) {
|
||||
break
|
||||
}
|
||||
Logger.log.info("Added/changed resource with filename: " + local.fileName)
|
||||
local.clearDirty(chunkPushItems[i].etag)
|
||||
i++
|
||||
}
|
||||
if (left > 0) {
|
||||
localDirty = localDirty.drop(left)
|
||||
chunkPushItems.drop(left)
|
||||
}
|
||||
|
||||
if (pushed > 0) {
|
||||
Logger.log.severe("Unprocessed localentries left, this should never happen!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun itemUpdateMtime(item: Item) {
|
||||
val meta = item.meta
|
||||
meta.setMtime(System.currentTimeMillis())
|
||||
item.meta = meta
|
||||
}
|
||||
|
||||
private fun createPushItems(): List<Item> {
|
||||
val ret = LinkedList<Item>()
|
||||
val colUid = cachedCollection.col.uid
|
||||
|
||||
synchronized(etebaseLocalCache) {
|
||||
for (local in localDeleted!!) {
|
||||
val item = etebaseLocalCache.itemGet(itemMgr, colUid, local.fileName!!)!!.item
|
||||
itemUpdateMtime(item)
|
||||
item.delete()
|
||||
ret.add(item)
|
||||
|
||||
if (ret.size == MAX_PUSH) {
|
||||
return ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized(etebaseLocalCache) {
|
||||
for (local in localDirty) {
|
||||
val cacheItem = if (local.fileName != null) etebaseLocalCache.itemGet(itemMgr, colUid, local.fileName!!) else null
|
||||
val item: Item
|
||||
if (cacheItem != null) {
|
||||
item = cacheItem.item
|
||||
itemUpdateMtime(item)
|
||||
} else {
|
||||
val uid = UUID.randomUUID().toString()
|
||||
val meta = ItemMetadata()
|
||||
meta.name = uid
|
||||
meta.mtime = System.currentTimeMillis()
|
||||
item = itemMgr.create(meta, "")
|
||||
|
||||
local.prepareForUpload(item.uid, uid)
|
||||
}
|
||||
|
||||
try {
|
||||
item.setContent(local.content)
|
||||
} catch (e: Exception) {
|
||||
Logger.log.warning("Failed creating local entry ${local.uuid}")
|
||||
if (local is LocalContact) {
|
||||
Logger.log.warning("Contact with title ${local.contact?.displayName}")
|
||||
} else if (local is LocalEvent) {
|
||||
Logger.log.warning("Event with title ${local.event?.summary}")
|
||||
} else if (local is LocalTask) {
|
||||
Logger.log.warning("Task with title ${local.task?.summary}")
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
ret.add(item)
|
||||
|
||||
if (ret.size == MAX_PUSH) {
|
||||
return ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class, IOException::class)
|
||||
protected open fun createLocalEntries() {
|
||||
private fun createLocalEntries() {
|
||||
localEntries = LinkedList()
|
||||
|
||||
// Not saving, just creating a fake one until we load it from a local db
|
||||
@ -510,7 +757,7 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
/**
|
||||
*/
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class, FileNotFoundException::class)
|
||||
protected fun prepareLocal() {
|
||||
protected open fun prepareLocal() {
|
||||
localDeleted = processLocallyDeleted()
|
||||
localDirty = localCollection!!.findDirty(MAX_PUSH)
|
||||
// This is done after fetching the local dirty so all the ones we are using will be prepared
|
||||
@ -527,7 +774,8 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
val localList = localCollection!!.findDeleted()
|
||||
val ret = ArrayList<T>(localList.size)
|
||||
|
||||
if (journalEntity.isReadOnly) {
|
||||
val readOnly = (isLegacy && journalEntity.isReadOnly) || (!isLegacy && (cachedCollection.col.accessLevel == CollectionAccessLevel.ReadOnly))
|
||||
if (readOnly) {
|
||||
for (local in localList) {
|
||||
Logger.log.info("Restoring locally deleted resource on a read only collection: ${local.uuid}")
|
||||
local.resetDeleted()
|
||||
@ -541,8 +789,11 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
if (local.uuid != null) {
|
||||
Logger.log.info(local.uuid + " has been deleted locally -> deleting from server")
|
||||
} else {
|
||||
Logger.log.fine("Entry deleted before ever syncing - genarting a UUID")
|
||||
local.prepareForUpload()
|
||||
if (isLegacy) {
|
||||
// It's done later for non-legacy
|
||||
Logger.log.fine("Entry deleted before ever syncing - genarting a UUID")
|
||||
local.legacyPrepareForUpload(null)
|
||||
}
|
||||
}
|
||||
|
||||
ret.add(local)
|
||||
@ -556,20 +807,22 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
protected open fun prepareDirty() {
|
||||
if (journalEntity.isReadOnly) {
|
||||
val readOnly = (isLegacy && journalEntity.isReadOnly) || (!isLegacy && (cachedCollection.col.accessLevel == CollectionAccessLevel.ReadOnly))
|
||||
if (readOnly) {
|
||||
for (local in localDirty) {
|
||||
Logger.log.info("Restoring locally modified resource on a read only collection: ${local.uuid}")
|
||||
if (local.uuid == null) {
|
||||
// If it was only local, delete.
|
||||
local.delete()
|
||||
} else {
|
||||
local.clearDirty(local.uuid!!)
|
||||
local.clearDirty(null)
|
||||
}
|
||||
numDiscarded++
|
||||
}
|
||||
|
||||
localDirty = LinkedList()
|
||||
} else {
|
||||
} else if (isLegacy) {
|
||||
// It's done later for non-legacy
|
||||
// assign file names and UIDs to new entries
|
||||
Logger.log.info("Looking for local entries without a uuid")
|
||||
for (local in localDirty) {
|
||||
@ -578,7 +831,7 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
}
|
||||
|
||||
Logger.log.fine("Found local record without file name; generating file name/UID if necessary")
|
||||
local.prepareForUpload()
|
||||
local.legacyPrepareForUpload(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import com.etebase.client.exceptions.*
|
||||
import com.etesync.syncadapter.AccountSettings
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
@ -27,7 +28,8 @@ class SyncNotification(internal val context: Context, internal val notificationT
|
||||
internal val notificationManager: NotificationManagerCompat
|
||||
lateinit var detailsIntent: Intent
|
||||
internal set
|
||||
internal var messageString: Int = 0
|
||||
internal var messageInt: Int = 0
|
||||
internal var messageString: String? = null
|
||||
|
||||
private var throwable: Throwable? = null
|
||||
|
||||
@ -37,30 +39,33 @@ class SyncNotification(internal val context: Context, internal val notificationT
|
||||
|
||||
fun setThrowable(e: Throwable) {
|
||||
throwable = e
|
||||
if (e is Exceptions.UnauthorizedException) {
|
||||
if (e is Exceptions.UnauthorizedException || e is UnauthorizedException) {
|
||||
Logger.log.log(Level.SEVERE, "Not authorized anymore", e)
|
||||
messageString = R.string.sync_error_unauthorized
|
||||
messageInt = R.string.sync_error_unauthorized
|
||||
} else if (e is Exceptions.UserInactiveException) {
|
||||
Logger.log.log(Level.SEVERE, "User inactive")
|
||||
messageString = R.string.sync_error_user_inactive
|
||||
} else if (e is Exceptions.ServiceUnavailableException) {
|
||||
messageInt = R.string.sync_error_user_inactive
|
||||
} else if (e is Exceptions.ServiceUnavailableException || e is TemporaryServerErrorException) {
|
||||
Logger.log.log(Level.SEVERE, "Service unavailable")
|
||||
messageString = R.string.sync_error_unavailable
|
||||
messageInt = R.string.sync_error_unavailable
|
||||
} else if (e is Exceptions.ReadOnlyException) {
|
||||
Logger.log.log(Level.SEVERE, "Journal is read only", e)
|
||||
messageString = R.string.sync_error_journal_readonly
|
||||
} else if (e is Exceptions.HttpException) {
|
||||
messageInt = R.string.sync_error_journal_readonly
|
||||
} else if (e is PermissionDeniedException) {
|
||||
Logger.log.log(Level.SEVERE, "Permission denied", e)
|
||||
messageString = context.getString(R.string.sync_error_permission_denied, e.localizedMessage)
|
||||
} else if (e is Exceptions.HttpException || e is ServerErrorException) {
|
||||
Logger.log.log(Level.SEVERE, "HTTP Exception during sync", e)
|
||||
messageString = R.string.sync_error_http_dav
|
||||
messageInt = R.string.sync_error_http_dav
|
||||
} else if (e is CalendarStorageException || e is ContactsStorageException || e is SQLiteException) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't access local storage", e)
|
||||
messageString = R.string.sync_error_local_storage
|
||||
messageInt = R.string.sync_error_local_storage
|
||||
} else if (e is Exceptions.IntegrityException) {
|
||||
Logger.log.log(Level.SEVERE, "Integrity error", e)
|
||||
messageString = R.string.sync_error_integrity
|
||||
messageInt = R.string.sync_error_integrity
|
||||
} else {
|
||||
Logger.log.log(Level.SEVERE, "Unknown sync error", e)
|
||||
messageString = R.string.sync_error
|
||||
messageInt = R.string.sync_error
|
||||
}
|
||||
|
||||
detailsIntent = Intent(context, NotificationHandlerActivity::class.java)
|
||||
@ -69,7 +74,7 @@ class SyncNotification(internal val context: Context, internal val notificationT
|
||||
}
|
||||
|
||||
fun notify(title: String, state: String) {
|
||||
val message = context.getString(messageString, state)
|
||||
val message = messageString ?: context.getString(messageInt, state)
|
||||
notify(title, message, null, detailsIntent)
|
||||
}
|
||||
|
||||
|
@ -18,10 +18,7 @@ import android.os.Bundle
|
||||
import at.bitfire.ical4android.AndroidTaskList
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import at.bitfire.ical4android.TaskProvider.ProviderName
|
||||
import com.etesync.syncadapter.AccountSettings
|
||||
import com.etesync.syncadapter.App
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.*
|
||||
import com.etesync.syncadapter.log.Logger
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
import com.etesync.syncadapter.model.JournalEntity
|
||||
@ -42,9 +39,6 @@ class TasksSyncAdapterService: SyncAdapterService() {
|
||||
context: Context,
|
||||
private val name: ProviderName
|
||||
): SyncAdapter(context) {
|
||||
override val syncErrorTitle = R.string.sync_error_tasks
|
||||
override val notificationManager = SyncNotification(context, "journals-tasks", Constants.NOTIFICATION_TASK_SYNC)
|
||||
|
||||
override fun onPerformSyncDo(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
|
||||
val taskProvider = TaskProvider.fromProviderClient(context, provider, name)
|
||||
@ -63,8 +57,11 @@ class TasksSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
RefreshCollections(account, CollectionInfo.Type.TASKS).run()
|
||||
|
||||
updateLocalTaskLists(taskProvider, account, accountSettings)
|
||||
|
||||
if (accountSettings.isLegacy) {
|
||||
legacyUpdateLocalTaskLists(taskProvider, account, accountSettings)
|
||||
} else {
|
||||
updateLocalTaskLists(taskProvider, account, accountSettings)
|
||||
}
|
||||
val principal = accountSettings.uri?.toHttpUrlOrNull()!!
|
||||
|
||||
for (taskList in AndroidTaskList.find(account, taskProvider, LocalTaskList.Factory, "${TaskContract.TaskLists.SYNC_ENABLED}!=0", null)) {
|
||||
@ -78,6 +75,50 @@ class TasksSyncAdapterService: SyncAdapterService() {
|
||||
}
|
||||
|
||||
private fun updateLocalTaskLists(provider: TaskProvider, account: Account, settings: AccountSettings) {
|
||||
val remote = HashMap<String, CachedCollection>()
|
||||
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
|
||||
val collections: List<CachedCollection>
|
||||
synchronized(etebaseLocalCache) {
|
||||
val httpClient = HttpClient.Builder(context, settings).setForeground(false).build()
|
||||
val etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
|
||||
val colMgr = etebase.collectionManager
|
||||
|
||||
collections = etebaseLocalCache.collectionList(colMgr).filter { it.meta.collectionType == Constants.ETEBASE_TYPE_TASKS }
|
||||
}
|
||||
|
||||
for (collection in collections) {
|
||||
remote[collection.col.uid] = collection
|
||||
}
|
||||
|
||||
val local = AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null)
|
||||
|
||||
val updateColors = settings.manageCalendarColors
|
||||
|
||||
// delete obsolete local calendar
|
||||
for (taskList in local) {
|
||||
val url = taskList.syncId
|
||||
val collection = remote[url]
|
||||
if (collection == null) {
|
||||
Logger.log.fine("Deleting obsolete local taskList $url")
|
||||
taskList.delete()
|
||||
} else {
|
||||
// remote CollectionInfo found for this local collection, update data
|
||||
Logger.log.fine("Updating local taskList $url")
|
||||
taskList.update(collection, updateColors)
|
||||
// we already have a local taskList for this remote collection, don't take into consideration anymore
|
||||
remote.remove(url)
|
||||
}
|
||||
}
|
||||
|
||||
// create new local calendars
|
||||
for (url in remote.keys) {
|
||||
val cachedCollection = remote[url]!!
|
||||
Logger.log.info("Adding local calendar list $cachedCollection")
|
||||
LocalTaskList.create(account, provider, cachedCollection)
|
||||
}
|
||||
}
|
||||
|
||||
private fun legacyUpdateLocalTaskLists(provider: TaskProvider, account: Account, settings: AccountSettings) {
|
||||
val data = (context.applicationContext as App).data
|
||||
var service = JournalModel.Service.fetchOrCreate(data, account.name, CollectionInfo.Type.TASKS)
|
||||
|
||||
|
@ -13,6 +13,7 @@ import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import at.bitfire.ical4android.Task
|
||||
import com.etebase.client.Item
|
||||
import com.etesync.syncadapter.AccountSettings
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
@ -43,7 +44,7 @@ class TasksSyncManager(
|
||||
get() = context.getString(R.string.sync_error_tasks, account.name)
|
||||
|
||||
override val syncSuccessfullyTitle: String
|
||||
get() = context.getString(R.string.sync_successfully_tasks, info.displayName,
|
||||
get() = context.getString(R.string.sync_successfully_tasks, localTaskList().name!!,
|
||||
account.name)
|
||||
|
||||
init {
|
||||
@ -58,7 +59,9 @@ class TasksSyncManager(
|
||||
if (!super.prepare())
|
||||
return false
|
||||
|
||||
journal = JournalEntryManager(httpClient.okHttpClient, remote, localTaskList().url!!)
|
||||
if (isLegacy) {
|
||||
journal = JournalEntryManager(httpClient.okHttpClient, remote, localTaskList().url!!)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -68,6 +71,32 @@ class TasksSyncManager(
|
||||
return localCollection as LocalTaskList
|
||||
}
|
||||
|
||||
override fun processItem(item: Item) {
|
||||
val local = localCollection!!.findByFilename(item.uid)
|
||||
|
||||
if (!item.isDeleted) {
|
||||
val inputReader = StringReader(String(item.content))
|
||||
|
||||
val tasks = Task.tasksFromReader(inputReader)
|
||||
if (tasks.size == 0) {
|
||||
Logger.log.warning("Received VCard without data, ignoring")
|
||||
return
|
||||
} else if (tasks.size > 1) {
|
||||
Logger.log.warning("Received multiple VCALs, using first one")
|
||||
}
|
||||
|
||||
val task = tasks[0]
|
||||
processTask(item, task, local)
|
||||
} else {
|
||||
if (local != null) {
|
||||
Logger.log.info("Removing local record #" + local.id + " which has been deleted on the server")
|
||||
local.delete()
|
||||
} else {
|
||||
Logger.log.warning("Tried deleting a non-existent record: " + item.uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun processSyncEntryImpl(cEntry: SyncEntry) {
|
||||
val inputReader = StringReader(cEntry.content)
|
||||
|
||||
@ -83,7 +112,7 @@ class TasksSyncManager(
|
||||
val local = localCollection!!.findByUid(event.uid!!)
|
||||
|
||||
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
|
||||
processTask(event, local)
|
||||
legacyProcessTask(event, local)
|
||||
} else {
|
||||
if (local != null) {
|
||||
Logger.log.info("Removing local record #" + local.id + " which has been deleted on the server")
|
||||
@ -94,7 +123,25 @@ class TasksSyncManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun processTask(newData: Task, _localTask: LocalTask?): LocalTask {
|
||||
private fun processTask(item: Item, newData: Task, _localTask: LocalTask?): LocalTask {
|
||||
var localTask = _localTask
|
||||
// delete local Task, if it exists
|
||||
if (localTask != null) {
|
||||
Logger.log.info("Updating " + item.uid + " in local calendar")
|
||||
localTask.eTag = item.etag
|
||||
localTask.update(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
} else {
|
||||
Logger.log.info("Adding " + item.uid + " to local calendar")
|
||||
localTask = LocalTask(localTaskList(), newData, item.uid, item.etag)
|
||||
localTask.add()
|
||||
syncResult.stats.numInserts++
|
||||
}
|
||||
|
||||
return localTask
|
||||
}
|
||||
|
||||
private fun legacyProcessTask(newData: Task, _localTask: LocalTask?): LocalTask {
|
||||
var localTask = _localTask
|
||||
// delete local Task, if it exists
|
||||
if (localTask != null) {
|
||||
|
@ -25,20 +25,27 @@ import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import com.etesync.syncadapter.*
|
||||
import com.etebase.client.CollectionAccessLevel
|
||||
import com.etebase.client.CollectionManager
|
||||
import com.etebase.client.Utils
|
||||
import com.etebase.client.exceptions.EtebaseException
|
||||
import com.etesync.journalmanager.Crypto
|
||||
import com.etesync.journalmanager.Exceptions
|
||||
import com.etesync.journalmanager.JournalAuthenticator
|
||||
import com.etesync.syncadapter.*
|
||||
import com.etesync.syncadapter.Constants.*
|
||||
import com.etesync.syncadapter.log.Logger
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
import com.etesync.syncadapter.model.JournalEntity
|
||||
import com.etesync.syncadapter.model.MyEntityDataStore
|
||||
import com.etesync.syncadapter.model.ServiceEntity
|
||||
import com.etesync.syncadapter.resource.LocalAddressBook
|
||||
import com.etesync.syncadapter.resource.LocalCalendar
|
||||
import com.etesync.syncadapter.syncadapter.requestSync
|
||||
import com.etesync.syncadapter.ui.etebase.CollectionActivity
|
||||
import com.etesync.syncadapter.ui.etebase.InvitationsActivity
|
||||
import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment
|
||||
import com.etesync.syncadapter.utils.HintManager
|
||||
import com.etesync.syncadapter.utils.ShowcaseBuilder
|
||||
@ -52,6 +59,7 @@ import java.util.logging.Level
|
||||
class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener, LoaderManager.LoaderCallbacks<AccountActivity.AccountInfo>, Refreshable {
|
||||
|
||||
private lateinit var account: Account
|
||||
private lateinit var settings: AccountSettings
|
||||
private var accountInfo: AccountInfo? = null
|
||||
|
||||
internal var listCalDAV: ListView? = null
|
||||
@ -64,22 +72,30 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
private val onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, _ ->
|
||||
val list = parent as ListView
|
||||
val adapter = list.adapter as ArrayAdapter<*>
|
||||
val journalEntity = adapter.getItem(position) as JournalEntity
|
||||
val info = journalEntity.getInfo()
|
||||
val info = adapter.getItem(position) as CollectionListItemInfo
|
||||
|
||||
startActivity(ViewCollectionActivity.newIntent(this@AccountActivity, account, info))
|
||||
if (settings.isLegacy) {
|
||||
startActivity(ViewCollectionActivity.newIntent(this@AccountActivity, account, info.legacyInfo!!))
|
||||
} else {
|
||||
startActivity(CollectionActivity.newIntent(this@AccountActivity, account, info.uid))
|
||||
}
|
||||
}
|
||||
|
||||
private val formattedFingerprint: String?
|
||||
get() {
|
||||
try {
|
||||
val settings = AccountSettings(this, account)
|
||||
return Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(settings.keyPair!!.publicKey)
|
||||
if (settings.isLegacy) {
|
||||
val settings = AccountSettings(this, account)
|
||||
return Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(settings.keyPair!!.publicKey)
|
||||
} else {
|
||||
val etebase = EtebaseLocalCache.getEtebase(this, HttpClient.sharedClient, settings)
|
||||
val invitationManager = etebase.invitationManager
|
||||
return Utils.prettyFingerprint(invitationManager.pubkey)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
return e.localizedMessage
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -87,6 +103,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
|
||||
account = intent.getParcelableExtra(EXTRA_ACCOUNT)
|
||||
title = account.name
|
||||
settings = AccountSettings(this, account)
|
||||
|
||||
setContentView(R.layout.activity_account)
|
||||
|
||||
@ -131,13 +148,19 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
HintManager.setHintSeen(this, HINT_VIEW_COLLECTION, true)
|
||||
}
|
||||
|
||||
if (!SetupUserInfoFragment.hasUserInfo(this, account)) {
|
||||
SetupUserInfoFragment.newInstance(account).show(supportFragmentManager, null)
|
||||
if (settings.isLegacy) {
|
||||
if (!SetupUserInfoFragment.hasUserInfo(this, account)) {
|
||||
SetupUserInfoFragment.newInstance(account).show(supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.activity_account, menu)
|
||||
if (settings.isLegacy) {
|
||||
val invitations = menu.findItem(R.id.invitations)
|
||||
invitations.setVisible(false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -167,6 +190,10 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
.setPositiveButton(android.R.string.yes) { _, _ -> }.create()
|
||||
dialog.show()
|
||||
}
|
||||
R.id.invitations -> {
|
||||
val intent = InvitationsActivity.newIntent(this, account)
|
||||
startActivity(intent)
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
@ -195,19 +222,31 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
val info: CollectionInfo
|
||||
when (item.itemId) {
|
||||
R.id.create_calendar -> {
|
||||
info = CollectionInfo()
|
||||
info.enumType = CollectionInfo.Type.CALENDAR
|
||||
startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info))
|
||||
if (settings.isLegacy) {
|
||||
info = CollectionInfo()
|
||||
info.enumType = CollectionInfo.Type.CALENDAR
|
||||
startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info))
|
||||
} else {
|
||||
startActivity(CollectionActivity.newCreateCollectionIntent(this@AccountActivity, account, ETEBASE_TYPE_CALENDAR))
|
||||
}
|
||||
}
|
||||
R.id.create_tasklist -> {
|
||||
info = CollectionInfo()
|
||||
info.enumType = CollectionInfo.Type.TASKS
|
||||
startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info))
|
||||
if (settings.isLegacy) {
|
||||
info = CollectionInfo()
|
||||
info.enumType = CollectionInfo.Type.TASKS
|
||||
startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info))
|
||||
} else {
|
||||
startActivity(CollectionActivity.newCreateCollectionIntent(this@AccountActivity, account, ETEBASE_TYPE_TASKS))
|
||||
}
|
||||
}
|
||||
R.id.create_addressbook -> {
|
||||
info = CollectionInfo()
|
||||
info.enumType = CollectionInfo.Type.ADDRESS_BOOK
|
||||
startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info))
|
||||
if (settings.isLegacy) {
|
||||
info = CollectionInfo()
|
||||
info.enumType = CollectionInfo.Type.ADDRESS_BOOK
|
||||
startActivity(CreateCollectionActivity.newIntent(this@AccountActivity, account, info))
|
||||
} else {
|
||||
startActivity(CollectionActivity.newCreateCollectionIntent(this@AccountActivity, account, ETEBASE_TYPE_ADDRESS_BOOK))
|
||||
}
|
||||
}
|
||||
R.id.install_tasksorg -> {
|
||||
installPackage(tasksOrgPackage)
|
||||
@ -227,10 +266,9 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
internal var taskdav: ServiceInfo? = null
|
||||
|
||||
class ServiceInfo {
|
||||
internal var id: Long = 0
|
||||
internal var refreshing: Boolean = false
|
||||
|
||||
internal var journals: List<JournalEntity>? = null
|
||||
internal var infos: List<CollectionListItemInfo>? = null
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,7 +292,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
listCardDAV!!.setAlpha(if (info.carddav!!.refreshing) 0.5f else 1f)
|
||||
|
||||
val adapter = CollectionListAdapter(this, account)
|
||||
adapter.addAll(info.carddav!!.journals!!)
|
||||
adapter.addAll(info.carddav!!.infos!!)
|
||||
listCardDAV!!.adapter = adapter
|
||||
listCardDAV!!.onItemClickListener = onItemClickListener
|
||||
}
|
||||
@ -268,7 +306,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
listCalDAV!!.setAlpha(if (info.caldav!!.refreshing) 0.5f else 1f)
|
||||
|
||||
val adapter = CollectionListAdapter(this, account)
|
||||
adapter.addAll(info.caldav!!.journals!!)
|
||||
adapter.addAll(info.caldav!!.infos!!)
|
||||
listCalDAV!!.adapter = adapter
|
||||
listCalDAV!!.onItemClickListener = onItemClickListener
|
||||
}
|
||||
@ -282,7 +320,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
listTaskDAV!!.setAlpha(if (info.taskdav!!.refreshing) 0.5f else 1f)
|
||||
|
||||
val adapter = CollectionListAdapter(this, account)
|
||||
adapter.addAll(info.taskdav!!.journals!!)
|
||||
adapter.addAll(info.taskdav!!.infos!!)
|
||||
listTaskDAV!!.adapter = adapter
|
||||
listTaskDAV!!.onItemClickListener = onItemClickListener
|
||||
|
||||
@ -342,50 +380,121 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
forceLoad()
|
||||
}
|
||||
|
||||
private fun getLegacyJournals(data: MyEntityDataStore, serviceEntity: ServiceEntity): List<CollectionListItemInfo> {
|
||||
return JournalEntity.getJournals(data, serviceEntity).map {
|
||||
val info = it.info
|
||||
val isAdmin = it.isOwner(account.name)
|
||||
CollectionListItemInfo(it.uid, info.enumType!!, info.displayName!!, info.description ?: "", info.color, it.isReadOnly, isAdmin, info)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCollections(etebaseLocalCache: EtebaseLocalCache, colMgr: CollectionManager, type: CollectionInfo.Type): List<CollectionListItemInfo> {
|
||||
val strType = when (type) {
|
||||
CollectionInfo.Type.ADDRESS_BOOK -> ETEBASE_TYPE_ADDRESS_BOOK
|
||||
CollectionInfo.Type.CALENDAR -> ETEBASE_TYPE_CALENDAR
|
||||
CollectionInfo.Type.TASKS -> ETEBASE_TYPE_TASKS
|
||||
}
|
||||
|
||||
synchronized(etebaseLocalCache) {
|
||||
return etebaseLocalCache.collectionList(colMgr).map {
|
||||
val meta = it.meta
|
||||
|
||||
if (strType != meta.collectionType) {
|
||||
return@map null
|
||||
}
|
||||
|
||||
val accessLevel = it.col.accessLevel
|
||||
val isReadOnly = accessLevel == CollectionAccessLevel.ReadOnly
|
||||
val isAdmin = accessLevel == CollectionAccessLevel.Admin
|
||||
|
||||
val metaColor = meta.color
|
||||
val color = if (!metaColor.isNullOrBlank()) LocalCalendar.parseColor(metaColor) else null
|
||||
CollectionListItemInfo(it.col.uid, type, meta.name, meta.description
|
||||
?: "", color, isReadOnly, isAdmin, null)
|
||||
}.filterNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadInBackground(): AccountInfo {
|
||||
val info = AccountInfo()
|
||||
val settings: AccountSettings
|
||||
try {
|
||||
settings = AccountSettings(context, account)
|
||||
} catch (e: InvalidAccountException) {
|
||||
return info
|
||||
}
|
||||
if (settings.isLegacy) {
|
||||
val data = (context.applicationContext as App).data
|
||||
|
||||
val data = (context.applicationContext as App).data
|
||||
for (serviceEntity in data.select(ServiceEntity::class.java).where(ServiceEntity.ACCOUNT.eq(account.name)).get()) {
|
||||
val id = serviceEntity.id.toLong()
|
||||
val service = serviceEntity.type!!
|
||||
when (service) {
|
||||
CollectionInfo.Type.ADDRESS_BOOK -> {
|
||||
info.carddav = AccountInfo.ServiceInfo()
|
||||
info.carddav!!.refreshing = davService != null && davService!!.isRefreshing(id) || ContentResolver.isSyncActive(account, App.addressBooksAuthority)
|
||||
info.carddav!!.infos = getLegacyJournals(data, serviceEntity)
|
||||
|
||||
for (serviceEntity in data.select(ServiceEntity::class.java).where(ServiceEntity.ACCOUNT.eq(account.name)).get()) {
|
||||
val id = serviceEntity.id.toLong()
|
||||
val service = serviceEntity.type!!
|
||||
when (service) {
|
||||
CollectionInfo.Type.ADDRESS_BOOK -> {
|
||||
info.carddav = AccountInfo.ServiceInfo()
|
||||
info.carddav!!.id = id
|
||||
info.carddav!!.refreshing = davService != null && davService!!.isRefreshing(id) || ContentResolver.isSyncActive(account, App.addressBooksAuthority)
|
||||
info.carddav!!.journals = JournalEntity.getJournals(data, serviceEntity)
|
||||
val accountManager = AccountManager.get(context)
|
||||
for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) {
|
||||
val addressBook = LocalAddressBook(context, addrBookAccount, null)
|
||||
try {
|
||||
if (account == addressBook.mainAccount)
|
||||
info.carddav!!.refreshing = info.carddav!!.refreshing or ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY)
|
||||
} catch (e: ContactsStorageException) {
|
||||
}
|
||||
|
||||
val accountManager = AccountManager.get(context)
|
||||
for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) {
|
||||
val addressBook = LocalAddressBook(context, addrBookAccount, null)
|
||||
try {
|
||||
if (account == addressBook.mainAccount)
|
||||
info.carddav!!.refreshing = info.carddav!!.refreshing or ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY)
|
||||
} catch (e: ContactsStorageException) {
|
||||
}
|
||||
|
||||
}
|
||||
CollectionInfo.Type.CALENDAR -> {
|
||||
info.caldav = AccountInfo.ServiceInfo()
|
||||
info.caldav!!.refreshing = davService != null && davService!!.isRefreshing(id) ||
|
||||
ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY)
|
||||
info.caldav!!.infos = getLegacyJournals(data, serviceEntity)
|
||||
}
|
||||
CollectionInfo.Type.TASKS -> {
|
||||
info.taskdav = AccountInfo.ServiceInfo()
|
||||
info.taskdav!!.refreshing = davService != null && davService!!.isRefreshing(id) ||
|
||||
OPENTASK_PROVIDERS.any {
|
||||
ContentResolver.isSyncActive(account, it.authority)
|
||||
}
|
||||
info.taskdav!!.infos = getLegacyJournals(data, serviceEntity)
|
||||
}
|
||||
}
|
||||
CollectionInfo.Type.CALENDAR -> {
|
||||
info.caldav = AccountInfo.ServiceInfo()
|
||||
info.caldav!!.id = id
|
||||
info.caldav!!.refreshing = davService != null && davService!!.isRefreshing(id) ||
|
||||
ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY)
|
||||
info.caldav!!.journals = JournalEntity.getJournals(data, serviceEntity)
|
||||
}
|
||||
CollectionInfo.Type.TASKS -> {
|
||||
info.taskdav = AccountInfo.ServiceInfo()
|
||||
info.taskdav!!.id = id
|
||||
info.taskdav!!.refreshing = davService != null && davService!!.isRefreshing(id) ||
|
||||
OPENTASK_PROVIDERS.any {
|
||||
ContentResolver.isSyncActive(account, it.authority)
|
||||
}
|
||||
info.taskdav!!.journals = JournalEntity.getJournals(data, serviceEntity)
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
|
||||
val httpClient = HttpClient.Builder(context).build().okHttpClient
|
||||
val etebase = EtebaseLocalCache.getEtebase(context, httpClient, settings)
|
||||
val colMgr = etebase.collectionManager
|
||||
|
||||
info.carddav = AccountInfo.ServiceInfo()
|
||||
info.carddav!!.refreshing = ContentResolver.isSyncActive(account, App.addressBooksAuthority)
|
||||
info.carddav!!.infos = getCollections(etebaseLocalCache, colMgr, CollectionInfo.Type.ADDRESS_BOOK)
|
||||
|
||||
val accountManager = AccountManager.get(context)
|
||||
for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) {
|
||||
val addressBook = LocalAddressBook(context, addrBookAccount, null)
|
||||
try {
|
||||
if (account == addressBook.mainAccount)
|
||||
info.carddav!!.refreshing = info.carddav!!.refreshing or ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY)
|
||||
} catch (e: ContactsStorageException) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
info.caldav = AccountInfo.ServiceInfo()
|
||||
info.caldav!!.refreshing = ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY)
|
||||
info.caldav!!.infos = getCollections(etebaseLocalCache, colMgr, CollectionInfo.Type.CALENDAR)
|
||||
|
||||
info.taskdav = AccountInfo.ServiceInfo()
|
||||
info.taskdav!!.refreshing = OPENTASK_PROVIDERS.any {
|
||||
ContentResolver.isSyncActive(account, it.authority)
|
||||
}
|
||||
info.taskdav!!.infos = getCollections(etebaseLocalCache, colMgr, CollectionInfo.Type.TASKS)
|
||||
|
||||
return info
|
||||
}
|
||||
}
|
||||
@ -393,15 +502,16 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
|
||||
/* LIST ADAPTERS */
|
||||
|
||||
class CollectionListAdapter(context: Context, private val account: Account) : ArrayAdapter<JournalEntity>(context, R.layout.account_collection_item) {
|
||||
data class CollectionListItemInfo(val uid: String, val enumType: CollectionInfo.Type, val displayName: String, val description: String, val color: Int?, val isReadOnly: Boolean, val isAdmin: Boolean, val legacyInfo: CollectionInfo?)
|
||||
|
||||
class CollectionListAdapter(context: Context, private val account: Account) : ArrayAdapter<CollectionListItemInfo>(context, R.layout.account_collection_item) {
|
||||
|
||||
override fun getView(position: Int, _v: View?, parent: ViewGroup): View {
|
||||
var v = _v
|
||||
if (v == null)
|
||||
v = LayoutInflater.from(context).inflate(R.layout.account_collection_item, parent, false)
|
||||
|
||||
val journalEntity = getItem(position)
|
||||
val info = journalEntity!!.info
|
||||
val info = getItem(position)!!
|
||||
|
||||
var tv = v!!.findViewById<View>(R.id.title) as TextView
|
||||
tv.text = if (TextUtils.isEmpty(info.displayName)) info.uid else info.displayName
|
||||
@ -422,10 +532,10 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
}
|
||||
|
||||
val readOnly = v.findViewById<View>(R.id.read_only)
|
||||
readOnly.visibility = if (journalEntity.isReadOnly) View.VISIBLE else View.GONE
|
||||
readOnly.visibility = if (info.isReadOnly) View.VISIBLE else View.GONE
|
||||
|
||||
val shared = v.findViewById<View>(R.id.shared)
|
||||
val isOwner = journalEntity.isOwner(account.name)
|
||||
val isOwner = info.isAdmin
|
||||
shared.visibility = if (isOwner) View.GONE else View.VISIBLE
|
||||
|
||||
return v
|
||||
@ -437,17 +547,32 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
private fun deleteAccount() {
|
||||
val accountManager = AccountManager.get(this)
|
||||
val settings = AccountSettings(this@AccountActivity, account)
|
||||
val authToken = settings.authToken
|
||||
val principal = settings.uri?.toHttpUrlOrNull()
|
||||
|
||||
doAsync {
|
||||
try {
|
||||
val httpClient = HttpClient.Builder(this@AccountActivity, null, authToken).build().okHttpClient
|
||||
val journalAuthenticator = JournalAuthenticator(httpClient, principal!!)
|
||||
journalAuthenticator.invalidateAuthToken(authToken)
|
||||
} catch (e: Exceptions.HttpException) {
|
||||
// Ignore failures for now
|
||||
Logger.log.warning(e.toString())
|
||||
if (settings.isLegacy) {
|
||||
val authToken = settings.authToken
|
||||
val principal = settings.uri?.toHttpUrlOrNull()
|
||||
|
||||
try {
|
||||
val httpClient = HttpClient.Builder(this@AccountActivity, null, authToken).build().okHttpClient
|
||||
val journalAuthenticator = JournalAuthenticator(httpClient, principal!!)
|
||||
journalAuthenticator.invalidateAuthToken(authToken)
|
||||
} catch (e: Exceptions.HttpException) {
|
||||
// Ignore failures for now
|
||||
Logger.log.warning(e.toString())
|
||||
}
|
||||
} else {
|
||||
val etebaseLocalCache = EtebaseLocalCache.getInstance(this@AccountActivity, account.name)
|
||||
etebaseLocalCache.clearUserCache()
|
||||
|
||||
try {
|
||||
val httpClient = HttpClient.Builder(this@AccountActivity).build()
|
||||
val etebase = EtebaseLocalCache.getEtebase(this@AccountActivity, httpClient.okHttpClient, settings)
|
||||
etebase.logout()
|
||||
} catch(e: EtebaseException) {
|
||||
// Ignore failures for now
|
||||
Logger.log.warning(e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ import com.etesync.syncadapter.R
|
||||
class AccountListFragment : ListFragment(), LoaderManager.LoaderCallbacks<Array<Account>>, AdapterView.OnItemClickListener {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
listAdapter = AccountListAdapter(context!!)
|
||||
listAdapter = AccountListAdapter(requireContext())
|
||||
|
||||
return inflater.inflate(R.layout.account_list, container, false)
|
||||
}
|
||||
@ -58,7 +58,7 @@ class AccountListFragment : ListFragment(), LoaderManager.LoaderCallbacks<Array<
|
||||
// loader
|
||||
|
||||
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Array<Account>> {
|
||||
return AccountLoader(context!!)
|
||||
return AccountLoader(requireContext())
|
||||
}
|
||||
|
||||
override fun onLoadFinished(loader: Loader<Array<Account>>, accounts: Array<Account>) {
|
||||
|
@ -17,12 +17,13 @@ import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import android.text.TextUtils
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NavUtils
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.AsyncTaskLoader
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.preference.*
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS
|
||||
import com.etesync.syncadapter.*
|
||||
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
|
||||
@ -43,7 +44,8 @@ class AccountSettingsActivity : BaseActivity() {
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val frag = AccountSettingsFragment()
|
||||
val settings = AccountSettings(this, account)
|
||||
val frag: Fragment = if (settings.isLegacy) LegacyAccountSettingsFragment() else AccountSettingsFragment()
|
||||
frag.arguments = intent.extras
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(android.R.id.content, frag)
|
||||
@ -60,139 +62,226 @@ class AccountSettingsActivity : BaseActivity() {
|
||||
} else
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AccountSettingsFragment : PreferenceFragmentCompat(), LoaderManager.LoaderCallbacks<AccountSettings> {
|
||||
internal lateinit var account: Account
|
||||
class AccountSettingsFragment() : PreferenceFragmentCompat(), LoaderManager.LoaderCallbacks<AccountSettings> {
|
||||
internal lateinit var account: Account
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
account = arguments?.getParcelable(KEY_ACCOUNT)!!
|
||||
account = arguments?.getParcelable(KEY_ACCOUNT)!!
|
||||
|
||||
loaderManager.initLoader(0, arguments, this)
|
||||
loaderManager.initLoader(0, arguments, this)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(bundle: Bundle, s: String) {
|
||||
addPreferencesFromResource(R.xml.settings_account)
|
||||
}
|
||||
|
||||
override fun onCreateLoader(id: Int, args: Bundle?): Loader<AccountSettings> {
|
||||
return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account)
|
||||
}
|
||||
|
||||
override fun onLoadFinished(loader: Loader<AccountSettings>, settings: AccountSettings?) {
|
||||
if (settings == null) {
|
||||
activity!!.finish()
|
||||
return
|
||||
}
|
||||
// Category: dashboard
|
||||
val prefManageAccount = findPreference("manage_account")
|
||||
prefManageAccount.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
|
||||
Toast.makeText(requireContext(), "Not yet supported", Toast.LENGTH_LONG).show()
|
||||
true
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(bundle: Bundle, s: String) {
|
||||
addPreferencesFromResource(R.xml.settings_account)
|
||||
// Category: encryption
|
||||
val prefEncryptionPassword = findPreference("password")
|
||||
prefEncryptionPassword.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
|
||||
startActivity(ChangeEncryptionPasswordActivity.newIntent(activity!!, account))
|
||||
true
|
||||
}
|
||||
|
||||
override fun onCreateLoader(id: Int, args: Bundle?): Loader<AccountSettings> {
|
||||
return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account)
|
||||
}
|
||||
|
||||
override fun onLoadFinished(loader: Loader<AccountSettings>, settings: AccountSettings?) {
|
||||
if (settings == null) {
|
||||
activity!!.finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Category: dashboard
|
||||
val prefManageAccount = findPreference("manage_account")
|
||||
prefManageAccount.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
|
||||
WebViewActivity.openUrl(activity!!, Constants.dashboard.buildUpon().appendQueryParameter("email", account.name).build())
|
||||
true
|
||||
}
|
||||
|
||||
// category: authentication
|
||||
val prefPassword = findPreference("password") as EditTextPreference
|
||||
prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val credentials = if (newValue != null) LoginCredentials(settings.uri, account.name, newValue as String) else null
|
||||
LoginCredentialsChangeFragment.newInstance(account, credentials!!).show(fragmentManager!!, null)
|
||||
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
|
||||
false
|
||||
}
|
||||
|
||||
// Category: encryption
|
||||
val prefEncryptionPassword = findPreference("encryption_password")
|
||||
prefEncryptionPassword.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
|
||||
startActivity(ChangeEncryptionPasswordActivity.newIntent(activity!!, account))
|
||||
true
|
||||
}
|
||||
|
||||
val prefSync = findPreference("sync_interval") as ListPreference
|
||||
val syncInterval = settings.getSyncInterval(CalendarContract.AUTHORITY) // Calendar is the baseline interval
|
||||
if (syncInterval != null) {
|
||||
prefSync.value = syncInterval.toString()
|
||||
if (syncInterval == AccountSettings.SYNC_INTERVAL_MANUALLY)
|
||||
prefSync.setSummary(R.string.settings_sync_summary_manually)
|
||||
else
|
||||
prefSync.summary = getString(R.string.settings_sync_summary_periodically, prefSync.entry)
|
||||
prefSync.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val newInterval = java.lang.Long.parseLong(newValue as String)
|
||||
settings.setSyncInterval(App.addressBooksAuthority, newInterval)
|
||||
settings.setSyncInterval(CalendarContract.AUTHORITY, newInterval)
|
||||
OPENTASK_PROVIDERS.forEach {
|
||||
settings.setSyncInterval(it.authority, newInterval)
|
||||
}
|
||||
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
|
||||
false
|
||||
}
|
||||
} else {
|
||||
prefSync.isEnabled = false
|
||||
prefSync.setSummary(R.string.settings_sync_summary_not_available)
|
||||
}
|
||||
|
||||
val prefWifiOnly = findPreference("sync_wifi_only") as SwitchPreferenceCompat
|
||||
prefWifiOnly.isChecked = settings.syncWifiOnly
|
||||
prefWifiOnly.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, wifiOnly ->
|
||||
settings.setSyncWiFiOnly(wifiOnly as Boolean)
|
||||
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
|
||||
false
|
||||
}
|
||||
|
||||
val prefWifiOnlySSID = findPreference("sync_wifi_only_ssid") as EditTextPreference
|
||||
val onlySSID = settings.syncWifiOnlySSID
|
||||
prefWifiOnlySSID.text = onlySSID
|
||||
if (onlySSID != null)
|
||||
prefWifiOnlySSID.summary = getString(R.string.settings_sync_wifi_only_ssid_on, onlySSID)
|
||||
val prefSync = findPreference("sync_interval") as ListPreference
|
||||
val syncInterval = settings.getSyncInterval(CalendarContract.AUTHORITY) // Calendar is the baseline interval
|
||||
if (syncInterval != null) {
|
||||
prefSync.value = syncInterval.toString()
|
||||
if (syncInterval == AccountSettings.SYNC_INTERVAL_MANUALLY)
|
||||
prefSync.setSummary(R.string.settings_sync_summary_manually)
|
||||
else
|
||||
prefWifiOnlySSID.setSummary(R.string.settings_sync_wifi_only_ssid_off)
|
||||
prefWifiOnlySSID.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val ssid = newValue as String
|
||||
settings.syncWifiOnlySSID = if (!TextUtils.isEmpty(ssid)) ssid else null
|
||||
prefSync.summary = getString(R.string.settings_sync_summary_periodically, prefSync.entry)
|
||||
prefSync.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val newInterval = java.lang.Long.parseLong(newValue as String)
|
||||
settings.setSyncInterval(App.addressBooksAuthority, newInterval)
|
||||
settings.setSyncInterval(CalendarContract.AUTHORITY, newInterval)
|
||||
OPENTASK_PROVIDERS.forEach {
|
||||
settings.setSyncInterval(it.authority, newInterval)
|
||||
}
|
||||
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
|
||||
false
|
||||
}
|
||||
} else {
|
||||
prefSync.isEnabled = false
|
||||
prefSync.setSummary(R.string.settings_sync_summary_not_available)
|
||||
}
|
||||
|
||||
override fun onLoaderReset(loader: Loader<AccountSettings>) {}
|
||||
val prefWifiOnly = findPreference("sync_wifi_only") as SwitchPreferenceCompat
|
||||
prefWifiOnly.isChecked = settings.syncWifiOnly
|
||||
prefWifiOnly.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, wifiOnly ->
|
||||
settings.setSyncWiFiOnly(wifiOnly as Boolean)
|
||||
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
|
||||
false
|
||||
}
|
||||
|
||||
val prefWifiOnlySSID = findPreference("sync_wifi_only_ssid") as EditTextPreference
|
||||
val onlySSID = settings.syncWifiOnlySSID
|
||||
prefWifiOnlySSID.text = onlySSID
|
||||
if (onlySSID != null)
|
||||
prefWifiOnlySSID.summary = getString(R.string.settings_sync_wifi_only_ssid_on, onlySSID)
|
||||
else
|
||||
prefWifiOnlySSID.setSummary(R.string.settings_sync_wifi_only_ssid_off)
|
||||
prefWifiOnlySSID.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val ssid = newValue as String
|
||||
settings.syncWifiOnlySSID = if (!TextUtils.isEmpty(ssid)) ssid else null
|
||||
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class AccountSettingsLoader(context: Context, internal val account: Account) : AsyncTaskLoader<AccountSettings>(context), SyncStatusObserver {
|
||||
internal lateinit var listenerHandle: Any
|
||||
|
||||
override fun onStartLoading() {
|
||||
forceLoad()
|
||||
listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this)
|
||||
}
|
||||
|
||||
override fun onStopLoading() {
|
||||
ContentResolver.removeStatusChangeListener(listenerHandle)
|
||||
}
|
||||
|
||||
override fun abandon() {
|
||||
onStopLoading()
|
||||
}
|
||||
|
||||
override fun loadInBackground(): AccountSettings? {
|
||||
val settings: AccountSettings
|
||||
try {
|
||||
settings = AccountSettings(context, account)
|
||||
} catch (e: InvalidAccountException) {
|
||||
return null
|
||||
}
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
override fun onStatusChanged(which: Int) {
|
||||
Logger.log.fine("Reloading account settings")
|
||||
forceLoad()
|
||||
}
|
||||
|
||||
}
|
||||
override fun onLoaderReset(loader: Loader<AccountSettings>) {}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
class LegacyAccountSettingsFragment : PreferenceFragmentCompat(), LoaderManager.LoaderCallbacks<AccountSettings> {
|
||||
internal lateinit var account: Account
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
account = arguments?.getParcelable(KEY_ACCOUNT)!!
|
||||
|
||||
loaderManager.initLoader(0, arguments, this)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(bundle: Bundle, s: String) {
|
||||
addPreferencesFromResource(R.xml.settings_account_legacy)
|
||||
}
|
||||
|
||||
override fun onCreateLoader(id: Int, args: Bundle?): Loader<AccountSettings> {
|
||||
return AccountSettingsLoader(context!!, args!!.getParcelable(KEY_ACCOUNT) as Account)
|
||||
}
|
||||
|
||||
override fun onLoadFinished(loader: Loader<AccountSettings>, settings: AccountSettings?) {
|
||||
if (settings == null) {
|
||||
activity!!.finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Category: dashboard
|
||||
val prefManageAccount = findPreference("manage_account")
|
||||
prefManageAccount.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
|
||||
WebViewActivity.openUrl(activity!!, Constants.dashboard.buildUpon().appendQueryParameter("email", account.name).build())
|
||||
true
|
||||
}
|
||||
|
||||
// category: authentication
|
||||
val prefPassword = findPreference("password") as EditTextPreference
|
||||
prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val credentials = if (newValue != null) LoginCredentials(settings.uri, account.name, newValue as String) else null
|
||||
LoginCredentialsChangeFragment.newInstance(account, credentials!!).show(fragmentManager!!, null)
|
||||
loaderManager.restartLoader(0, arguments, this@LegacyAccountSettingsFragment)
|
||||
false
|
||||
}
|
||||
|
||||
// Category: encryption
|
||||
val prefEncryptionPassword = findPreference("encryption_password")
|
||||
prefEncryptionPassword.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
|
||||
startActivity(ChangeEncryptionPasswordActivity.newIntent(activity!!, account))
|
||||
true
|
||||
}
|
||||
|
||||
val prefSync = findPreference("sync_interval") as ListPreference
|
||||
val syncInterval = settings.getSyncInterval(CalendarContract.AUTHORITY) // Calendar is the baseline interval
|
||||
if (syncInterval != null) {
|
||||
prefSync.value = syncInterval.toString()
|
||||
if (syncInterval == AccountSettings.SYNC_INTERVAL_MANUALLY)
|
||||
prefSync.setSummary(R.string.settings_sync_summary_manually)
|
||||
else
|
||||
prefSync.summary = getString(R.string.settings_sync_summary_periodically, prefSync.entry)
|
||||
prefSync.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val newInterval = java.lang.Long.parseLong(newValue as String)
|
||||
settings.setSyncInterval(App.addressBooksAuthority, newInterval)
|
||||
settings.setSyncInterval(CalendarContract.AUTHORITY, newInterval)
|
||||
OPENTASK_PROVIDERS.forEach {
|
||||
settings.setSyncInterval(it.authority, newInterval)
|
||||
}
|
||||
loaderManager.restartLoader(0, arguments, this@LegacyAccountSettingsFragment)
|
||||
false
|
||||
}
|
||||
} else {
|
||||
prefSync.isEnabled = false
|
||||
prefSync.setSummary(R.string.settings_sync_summary_not_available)
|
||||
}
|
||||
|
||||
val prefWifiOnly = findPreference("sync_wifi_only") as SwitchPreferenceCompat
|
||||
prefWifiOnly.isChecked = settings.syncWifiOnly
|
||||
prefWifiOnly.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, wifiOnly ->
|
||||
settings.setSyncWiFiOnly(wifiOnly as Boolean)
|
||||
loaderManager.restartLoader(0, arguments, this@LegacyAccountSettingsFragment)
|
||||
false
|
||||
}
|
||||
|
||||
val prefWifiOnlySSID = findPreference("sync_wifi_only_ssid") as EditTextPreference
|
||||
val onlySSID = settings.syncWifiOnlySSID
|
||||
prefWifiOnlySSID.text = onlySSID
|
||||
if (onlySSID != null)
|
||||
prefWifiOnlySSID.summary = getString(R.string.settings_sync_wifi_only_ssid_on, onlySSID)
|
||||
else
|
||||
prefWifiOnlySSID.setSummary(R.string.settings_sync_wifi_only_ssid_off)
|
||||
prefWifiOnlySSID.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val ssid = newValue as String
|
||||
settings.syncWifiOnlySSID = if (!TextUtils.isEmpty(ssid)) ssid else null
|
||||
loaderManager.restartLoader(0, arguments, this@LegacyAccountSettingsFragment)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoaderReset(loader: Loader<AccountSettings>) {}
|
||||
}
|
||||
|
||||
private class AccountSettingsLoader(context: Context, internal val account: Account) : AsyncTaskLoader<AccountSettings>(context), SyncStatusObserver {
|
||||
internal lateinit var listenerHandle: Any
|
||||
|
||||
override fun onStartLoading() {
|
||||
forceLoad()
|
||||
listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this)
|
||||
}
|
||||
|
||||
override fun onStopLoading() {
|
||||
ContentResolver.removeStatusChangeListener(listenerHandle)
|
||||
}
|
||||
|
||||
override fun abandon() {
|
||||
onStopLoading()
|
||||
}
|
||||
|
||||
override fun loadInBackground(): AccountSettings? {
|
||||
val settings: AccountSettings
|
||||
try {
|
||||
settings = AccountSettings(context, account)
|
||||
} catch (e: InvalidAccountException) {
|
||||
return null
|
||||
}
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
override fun onStatusChanged(which: Int) {
|
||||
Logger.log.fine("Reloading account settings")
|
||||
forceLoad()
|
||||
}
|
||||
|
||||
}
|
@ -15,6 +15,7 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.etebase.client.Client
|
||||
import com.etesync.syncadapter.AccountSettings
|
||||
import com.etesync.syncadapter.HttpClient
|
||||
import com.etesync.syncadapter.R
|
||||
@ -53,7 +54,7 @@ open class ChangeEncryptionPasswordActivity : BaseActivity() {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.wrong_encryption_password)
|
||||
.setIcon(R.drawable.ic_error_dark)
|
||||
.setMessage(getString(R.string.wrong_encryption_password_content, e.localizedMessage))
|
||||
.setMessage(e.localizedMessage)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
// dismiss
|
||||
}.show()
|
||||
@ -62,6 +63,45 @@ open class ChangeEncryptionPasswordActivity : BaseActivity() {
|
||||
fun changePasswordDo(old_password: String, new_password: String) {
|
||||
val settings = AccountSettings(this, account)
|
||||
|
||||
if (settings.isLegacy) {
|
||||
legacyChangePasswordDo(settings, old_password, new_password)
|
||||
return
|
||||
}
|
||||
|
||||
doAsync {
|
||||
val httpClient = HttpClient.Builder(this@ChangeEncryptionPasswordActivity).setForeground(true).build().okHttpClient
|
||||
|
||||
try {
|
||||
Logger.log.info("Loging in with old password")
|
||||
val client = Client.create(httpClient, settings.uri?.toString())
|
||||
val etebase = com.etebase.client.Account.login(client, account.name, old_password)
|
||||
Logger.log.info("Login successful")
|
||||
|
||||
etebase.changePassword(new_password)
|
||||
|
||||
settings.etebaseSession = etebase.save(null)
|
||||
|
||||
uiThread {
|
||||
progress.dismiss()
|
||||
AlertDialog.Builder(this@ChangeEncryptionPasswordActivity)
|
||||
.setTitle(R.string.change_encryption_password_success_title)
|
||||
.setMessage(R.string.change_encryption_password_success_body)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
this@ChangeEncryptionPasswordActivity.finish()
|
||||
}.show()
|
||||
|
||||
requestSync(applicationContext, account)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
uiThread {
|
||||
changePasswordError(e)
|
||||
}
|
||||
return@doAsync
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun legacyChangePasswordDo(settings: AccountSettings, old_password: String, new_password: String) {
|
||||
doAsync {
|
||||
val httpClient = HttpClient.Builder(this@ChangeEncryptionPasswordActivity, settings).setForeground(false).build().okHttpClient
|
||||
|
||||
|
@ -106,13 +106,13 @@ class CollectionMembersListFragment : ListFragment(), AdapterView.OnItemClickLis
|
||||
override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
|
||||
val member = listAdapter?.getItem(position) as JournalManager.Member
|
||||
|
||||
AlertDialog.Builder(activity!!)
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setIcon(R.drawable.ic_info_dark)
|
||||
.setTitle(R.string.collection_members_remove_title)
|
||||
.setMessage(getString(R.string.collection_members_remove, member.user))
|
||||
.setPositiveButton(android.R.string.yes) { dialog, which ->
|
||||
val frag = RemoveMemberFragment.newInstance(account, info, member.user!!)
|
||||
frag.show(fragmentManager!!, null)
|
||||
frag.show(requireFragmentManager(), null)
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { dialog, which -> }.show()
|
||||
}
|
||||
|
@ -0,0 +1,170 @@
|
||||
package com.etesync.syncadapter.ui.etebase
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.observe
|
||||
import com.etebase.client.CollectionManager
|
||||
import com.etebase.client.CollectionMetadata
|
||||
import com.etesync.syncadapter.*
|
||||
import com.etesync.syncadapter.ui.BaseActivity
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
|
||||
class CollectionActivity() : BaseActivity() {
|
||||
private lateinit var account: Account
|
||||
private val model: AccountViewModel by viewModels()
|
||||
private val collectionModel: CollectionViewModel by viewModels()
|
||||
private val itemsModel: ItemsViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
account = intent.extras!!.getParcelable(EXTRA_ACCOUNT)!!
|
||||
val colUid = intent.extras!!.getString(EXTRA_COLLECTION_UID)
|
||||
val colType = intent.extras!!.getString(EXTRA_COLLECTION_TYPE)
|
||||
|
||||
setContentView(R.layout.etebase_collection_activity)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
model.loadAccount(this, account)
|
||||
if (colUid != null) {
|
||||
model.observe(this) {
|
||||
collectionModel.loadCollection(it, colUid)
|
||||
collectionModel.observe(this) { cachedCollection ->
|
||||
itemsModel.loadItems(it, cachedCollection)
|
||||
}
|
||||
}
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.fragment_container, ViewCollectionFragment())
|
||||
}
|
||||
} else if (colType != null) {
|
||||
model.observe(this) {
|
||||
doAsync {
|
||||
val meta = CollectionMetadata(colType, "")
|
||||
val cachedCollection = CachedCollection(it.colMgr.create(meta, ""), meta)
|
||||
uiThread {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.fragment_container, EditCollectionFragment(cachedCollection, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val EXTRA_ACCOUNT = "account"
|
||||
private val EXTRA_COLLECTION_UID = "collectionUid"
|
||||
private val EXTRA_COLLECTION_TYPE = "collectionType"
|
||||
|
||||
fun newIntent(context: Context, account: Account, colUid: String): Intent {
|
||||
val intent = Intent(context, CollectionActivity::class.java)
|
||||
intent.putExtra(EXTRA_ACCOUNT, account)
|
||||
intent.putExtra(EXTRA_COLLECTION_UID, colUid)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun newCreateCollectionIntent(context: Context, account: Account, colType: String): Intent {
|
||||
val intent = Intent(context, CollectionActivity::class.java)
|
||||
intent.putExtra(EXTRA_ACCOUNT, account)
|
||||
intent.putExtra(EXTRA_COLLECTION_TYPE, colType)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AccountViewModel : ViewModel() {
|
||||
private val holder = MutableLiveData<AccountHolder>()
|
||||
|
||||
fun loadAccount(context: Context, account: Account) {
|
||||
doAsync {
|
||||
val settings = AccountSettings(context, account)
|
||||
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
|
||||
val httpClient = HttpClient.Builder(context).setForeground(true).build().okHttpClient
|
||||
val etebase = EtebaseLocalCache.getEtebase(context, httpClient, settings)
|
||||
val colMgr = etebase.collectionManager
|
||||
uiThread {
|
||||
holder.value = AccountHolder(
|
||||
account,
|
||||
etebaseLocalCache,
|
||||
etebase,
|
||||
colMgr
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun observe(owner: LifecycleOwner, observer: (AccountHolder) -> Unit) =
|
||||
holder.observe(owner, observer)
|
||||
|
||||
val value: AccountHolder?
|
||||
get() = holder.value
|
||||
}
|
||||
|
||||
data class AccountHolder(val account: Account, val etebaseLocalCache: EtebaseLocalCache, val etebase: com.etebase.client.Account, val colMgr: CollectionManager)
|
||||
|
||||
class CollectionViewModel : ViewModel() {
|
||||
private val collection = MutableLiveData<CachedCollection>()
|
||||
|
||||
fun loadCollection(accountHolder: AccountHolder, colUid: String) {
|
||||
doAsync {
|
||||
val etebaseLocalCache = accountHolder.etebaseLocalCache
|
||||
val colMgr = accountHolder.colMgr
|
||||
val cachedCollection = synchronized(etebaseLocalCache) {
|
||||
etebaseLocalCache.collectionGet(colMgr, colUid)!!
|
||||
}
|
||||
uiThread {
|
||||
collection.value = cachedCollection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun observe(owner: LifecycleOwner, observer: (CachedCollection) -> Unit) =
|
||||
collection.observe(owner, observer)
|
||||
|
||||
val value: CachedCollection?
|
||||
get() = collection.value
|
||||
}
|
||||
|
||||
class ItemsViewModel : ViewModel() {
|
||||
private val cachedItems = MutableLiveData<List<CachedItem>>()
|
||||
|
||||
fun loadItems(accountCollectionHolder: AccountHolder, cachedCollection: CachedCollection) {
|
||||
doAsync {
|
||||
val col = cachedCollection.col
|
||||
val itemMgr = accountCollectionHolder.colMgr.getItemManager(col)
|
||||
val items = accountCollectionHolder.etebaseLocalCache.itemList(itemMgr, col.uid, withDeleted = true)
|
||||
uiThread {
|
||||
cachedItems.value = items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun observe(owner: LifecycleOwner, observer: (List<CachedItem>) -> Unit) =
|
||||
cachedItems.observe(owner, observer)
|
||||
|
||||
val value: List<CachedItem>?
|
||||
get() = cachedItems.value
|
||||
}
|
||||
|
||||
|
||||
class LoadingViewModel : ViewModel() {
|
||||
private val loading = MutableLiveData<Boolean>()
|
||||
|
||||
fun setLoading(value: Boolean) {
|
||||
loading.value = value
|
||||
}
|
||||
|
||||
fun observe(owner: LifecycleOwner, observer: (Boolean) -> Unit) =
|
||||
loading.observe(owner, observer)
|
||||
}
|
@ -0,0 +1,532 @@
|
||||
package com.etesync.syncadapter.ui.etebase
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
import android.text.format.DateFormat
|
||||
import android.text.format.DateUtils
|
||||
import android.view.*
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import at.bitfire.ical4android.Event
|
||||
import at.bitfire.ical4android.InvalidCalendarException
|
||||
import at.bitfire.ical4android.Task
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import at.bitfire.vcard4android.Contact
|
||||
import com.etesync.syncadapter.CachedCollection
|
||||
import com.etesync.syncadapter.CachedItem
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.resource.*
|
||||
import com.etesync.syncadapter.ui.BaseActivity
|
||||
import com.etesync.syncadapter.utils.EventEmailInvitation
|
||||
import com.etesync.syncadapter.utils.TaskProviderHandling
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import ezvcard.util.PartialDate
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.io.IOException
|
||||
import java.io.StringReader
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.Future
|
||||
|
||||
class CollectionItemFragment(private val cachedItem: CachedItem) : Fragment() {
|
||||
private val model: AccountViewModel by activityViewModels()
|
||||
private val collectionModel: CollectionViewModel by activityViewModels()
|
||||
|
||||
private var emailInvitationEvent: Event? = null
|
||||
private var emailInvitationEventString: String? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val ret = inflater.inflate(R.layout.journal_item_activity, container, false)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
collectionModel.observe(this) {
|
||||
(activity as? BaseActivity?)?.supportActionBar?.title = it.meta.name
|
||||
if (container != null) {
|
||||
initUi(inflater, ret, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
private fun initUi(inflater: LayoutInflater, v: View, cachedCollection: CachedCollection) {
|
||||
val viewPager = v.findViewById<ViewPager>(R.id.viewpager)
|
||||
viewPager.adapter = TabsAdapter(childFragmentManager, this, requireContext(), cachedCollection, cachedItem)
|
||||
|
||||
val tabLayout = v.findViewById<TabLayout>(R.id.tabs)
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
|
||||
v.findViewById<View>(R.id.journal_list_item).visibility = View.GONE
|
||||
}
|
||||
|
||||
fun allowSendEmail(event: Event?, icsContent: String) {
|
||||
emailInvitationEvent = event
|
||||
emailInvitationEventString = icsContent
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.collection_item_fragment, menu)
|
||||
menu.setGroupVisible(R.id.journal_item_menu_event_invite, emailInvitationEvent != null)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val accountHolder = model.value!!
|
||||
when (item.itemId) {
|
||||
R.id.on_send_event_invite -> {
|
||||
val account = accountHolder.account
|
||||
val intent = EventEmailInvitation(requireContext(), account).createIntent(emailInvitationEvent!!, emailInvitationEventString!!)
|
||||
startActivity(intent)
|
||||
}
|
||||
R.id.on_restore_item -> {
|
||||
restoreItem(accountHolder)
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
fun restoreItem(accountHolder: AccountHolder) {
|
||||
// FIXME: This code makes the assumption that providers are all available. May not be true for tasks, and potentially others too.
|
||||
val context = requireContext()
|
||||
val account = accountHolder.account
|
||||
val cachedCol = collectionModel.value!!
|
||||
when (cachedCol.meta.collectionType) {
|
||||
Constants.ETEBASE_TYPE_CALENDAR -> {
|
||||
val provider = context.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!
|
||||
val localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, cachedCol.col.uid)!!
|
||||
val event = Event.eventsFromReader(StringReader(cachedItem.content))[0]
|
||||
var localEvent = localCalendar.findByUid(event.uid!!)
|
||||
if (localEvent != null) {
|
||||
localEvent.updateAsDirty(event)
|
||||
} else {
|
||||
localEvent = LocalEvent(localCalendar, event, event.uid, null)
|
||||
localEvent.addAsDirty()
|
||||
}
|
||||
}
|
||||
Constants.ETEBASE_TYPE_TASKS -> {
|
||||
TaskProviderHandling.getWantedTaskSyncProvider(context)?.let {
|
||||
val provider = TaskProvider.acquire(context, it)!!
|
||||
val localTaskList = LocalTaskList.findByName(account, provider, LocalTaskList.Factory, cachedCol.col.uid)!!
|
||||
val task = Task.tasksFromReader(StringReader(cachedItem.content))[0]
|
||||
var localTask = localTaskList.findByUid(task.uid!!)
|
||||
if (localTask != null) {
|
||||
localTask.updateAsDirty(task)
|
||||
} else {
|
||||
localTask = LocalTask(localTaskList, task, task.uid, null)
|
||||
localTask.addAsDirty()
|
||||
}
|
||||
}
|
||||
}
|
||||
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
|
||||
val provider = context.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!
|
||||
val localAddressBook = LocalAddressBook.findByUid(context, provider, account, cachedCol.col.uid)!!
|
||||
val contact = Contact.fromReader(StringReader(cachedItem.content), null)[0]
|
||||
if (contact.group) {
|
||||
// FIXME: not currently supported
|
||||
} else {
|
||||
var localContact = localAddressBook.findByUid(contact.uid!!) as LocalContact?
|
||||
if (localContact != null) {
|
||||
localContact.updateAsDirty(contact)
|
||||
} else {
|
||||
localContact = LocalContact(localAddressBook, contact, contact.uid, null)
|
||||
localContact.createAsDirty()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setTitle(R.string.journal_item_restore_action)
|
||||
.setIcon(R.drawable.ic_restore_black)
|
||||
.setMessage(R.string.journal_item_restore_dialog_body)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, which ->
|
||||
// dismiss
|
||||
}
|
||||
.create()
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
private class TabsAdapter(fm: FragmentManager, private val mainFragment: CollectionItemFragment, private val context: Context, private val cachedCollection: CachedCollection, private val cachedItem: CachedItem) : FragmentPagerAdapter(fm) {
|
||||
|
||||
override fun getCount(): Int {
|
||||
// FIXME: Make it depend on info enumType (only have non-raw for known types)
|
||||
return 3
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence? {
|
||||
return if (position == 0) {
|
||||
context.getString(R.string.journal_item_tab_main)
|
||||
} else if (position == 1) {
|
||||
context.getString(R.string.journal_item_tab_raw)
|
||||
} else {
|
||||
context.getString(R.string.journal_item_tab_revisions)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
return if (position == 0) {
|
||||
PrettyFragment(mainFragment, cachedCollection, cachedItem.content)
|
||||
} else if (position == 1) {
|
||||
TextFragment(cachedItem.content)
|
||||
} else {
|
||||
ItemRevisionsListFragment(cachedCollection, cachedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TextFragment(private val content: String) : Fragment() {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val v = inflater.inflate(R.layout.text_fragment, container, false)
|
||||
|
||||
val tv = v.findViewById<View>(R.id.content) as TextView
|
||||
|
||||
tv.text = content
|
||||
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
class PrettyFragment(private val mainFragment: CollectionItemFragment, private val cachedCollection: CachedCollection, private val content: String) : Fragment() {
|
||||
private var asyncTask: Future<Unit>? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
var v: View? = null
|
||||
|
||||
when (cachedCollection.meta.collectionType) {
|
||||
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
|
||||
v = inflater.inflate(R.layout.contact_info, container, false)
|
||||
asyncTask = loadContactTask(v)
|
||||
}
|
||||
Constants.ETEBASE_TYPE_CALENDAR -> {
|
||||
v = inflater.inflate(R.layout.event_info, container, false)
|
||||
asyncTask = loadEventTask(v)
|
||||
}
|
||||
Constants.ETEBASE_TYPE_TASKS -> {
|
||||
v = inflater.inflate(R.layout.task_info, container, false)
|
||||
asyncTask = loadTaskTask(v)
|
||||
}
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
if (asyncTask != null)
|
||||
asyncTask!!.cancel(true)
|
||||
}
|
||||
|
||||
private fun loadEventTask(view: View): Future<Unit> {
|
||||
return doAsync {
|
||||
var event: Event? = null
|
||||
val inputReader = StringReader(content)
|
||||
|
||||
try {
|
||||
event = Event.eventsFromReader(inputReader, null)[0]
|
||||
} catch (e: InvalidCalendarException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
if (event != null) {
|
||||
uiThread {
|
||||
val loader = view.findViewById<View>(R.id.event_info_loading_msg)
|
||||
loader.visibility = View.GONE
|
||||
val contentContainer = view.findViewById<View>(R.id.event_info_scroll_view)
|
||||
contentContainer.visibility = View.VISIBLE
|
||||
|
||||
setTextViewText(view, R.id.title, event.summary)
|
||||
|
||||
setTextViewText(view, R.id.when_datetime, getDisplayedDatetime(event.dtStart?.date?.time!!, event.dtEnd?.date!!.time, event.isAllDay(), context))
|
||||
|
||||
setTextViewText(view, R.id.where, event.location)
|
||||
|
||||
val organizer = event.organizer
|
||||
if (organizer != null) {
|
||||
val tv = view.findViewById<View>(R.id.organizer) as TextView
|
||||
tv.text = organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "")
|
||||
} else {
|
||||
val organizerView = view.findViewById<View>(R.id.organizer_container)
|
||||
organizerView.visibility = View.GONE
|
||||
}
|
||||
|
||||
setTextViewText(view, R.id.description, event.description)
|
||||
|
||||
var first = true
|
||||
var sb = StringBuilder()
|
||||
for (attendee in event.attendees) {
|
||||
if (first) {
|
||||
first = false
|
||||
sb.append(getString(R.string.journal_item_attendees)).append(": ")
|
||||
} else {
|
||||
sb.append(", ")
|
||||
}
|
||||
sb.append(attendee.calAddress.toString().replaceFirst("mailto:".toRegex(), ""))
|
||||
}
|
||||
setTextViewText(view, R.id.attendees, sb.toString())
|
||||
|
||||
first = true
|
||||
sb = StringBuilder()
|
||||
for (alarm in event.alarms) {
|
||||
if (first) {
|
||||
first = false
|
||||
sb.append(getString(R.string.journal_item_reminders)).append(": ")
|
||||
} else {
|
||||
sb.append(", ")
|
||||
}
|
||||
sb.append(alarm.trigger.value)
|
||||
}
|
||||
setTextViewText(view, R.id.reminders, sb.toString())
|
||||
|
||||
if (event.attendees.isNotEmpty()) {
|
||||
mainFragment.allowSendEmail(event, content)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadTaskTask(view: View): Future<Unit> {
|
||||
return doAsync {
|
||||
var task: Task? = null
|
||||
val inputReader = StringReader(content)
|
||||
|
||||
try {
|
||||
task = Task.tasksFromReader(inputReader)[0]
|
||||
} catch (e: InvalidCalendarException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
if (task != null) {
|
||||
uiThread {
|
||||
val loader = view.findViewById<View>(R.id.task_info_loading_msg)
|
||||
loader.visibility = View.GONE
|
||||
val contentContainer = view.findViewById<View>(R.id.task_info_scroll_view)
|
||||
contentContainer.visibility = View.VISIBLE
|
||||
|
||||
setTextViewText(view, R.id.title, task.summary)
|
||||
|
||||
setTextViewText(view, R.id.where, task.location)
|
||||
|
||||
val organizer = task.organizer
|
||||
if (organizer != null) {
|
||||
val tv = view.findViewById<View>(R.id.organizer) as TextView
|
||||
tv.text = organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "")
|
||||
} else {
|
||||
val organizerView = view.findViewById<View>(R.id.organizer_container)
|
||||
organizerView.visibility = View.GONE
|
||||
}
|
||||
|
||||
setTextViewText(view, R.id.description, task.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadContactTask(view: View): Future<Unit> {
|
||||
return doAsync {
|
||||
var contact: Contact? = null
|
||||
val reader = StringReader(content)
|
||||
|
||||
try {
|
||||
contact = Contact.fromReader(reader, null)[0]
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
if (contact != null) {
|
||||
uiThread {
|
||||
val loader = view.findViewById<View>(R.id.loading_msg)
|
||||
loader.visibility = View.GONE
|
||||
val contentContainer = view.findViewById<View>(R.id.content_container)
|
||||
contentContainer.visibility = View.VISIBLE
|
||||
|
||||
val tv = view.findViewById<View>(R.id.display_name) as TextView
|
||||
tv.text = contact.displayName
|
||||
|
||||
if (contact.group) {
|
||||
showGroup(contact)
|
||||
} else {
|
||||
showContact(contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun showGroup(contact: Contact) {
|
||||
val view = requireView()
|
||||
|
||||
val mainCard = view.findViewById<View>(R.id.main_card) as ViewGroup
|
||||
|
||||
addInfoItem(view.context, mainCard, getString(R.string.journal_item_member_count), null, contact.members.size.toString())
|
||||
|
||||
for (member in contact.members) {
|
||||
addInfoItem(view.context, mainCard, getString(R.string.journal_item_member), null, member)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun showContact(contact: Contact) {
|
||||
val view = requireView()
|
||||
val mainCard = view.findViewById<View>(R.id.main_card) as ViewGroup
|
||||
val aboutCard = view.findViewById<View>(R.id.about_card) as ViewGroup
|
||||
aboutCard.findViewById<View>(R.id.title_container).visibility = View.VISIBLE
|
||||
|
||||
// TEL
|
||||
for (labeledPhone in contact.phoneNumbers) {
|
||||
val types = labeledPhone.property.types
|
||||
val type = if (types.size > 0) types[0].value else null
|
||||
addInfoItem(view.context, mainCard, getString(R.string.journal_item_phone), type, labeledPhone.property.text)
|
||||
}
|
||||
|
||||
// EMAIL
|
||||
for (labeledEmail in contact.emails) {
|
||||
val types = labeledEmail.property.types
|
||||
val type = if (types.size > 0) types[0].value else null
|
||||
addInfoItem(view.context, mainCard, getString(R.string.journal_item_email), type, labeledEmail.property.value)
|
||||
}
|
||||
|
||||
// ORG, TITLE, ROLE
|
||||
if (contact.organization != null) {
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_organization), contact.jobTitle, contact.organization?.values!![0])
|
||||
}
|
||||
if (contact.jobDescription != null) {
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_job_description), null, contact.jobTitle)
|
||||
}
|
||||
|
||||
// IMPP
|
||||
for (labeledImpp in contact.impps) {
|
||||
addInfoItem(view.context, mainCard, getString(R.string.journal_item_impp), labeledImpp.property.protocol, labeledImpp.property.handle)
|
||||
}
|
||||
|
||||
// NICKNAME
|
||||
if (contact.nickName != null && !contact.nickName?.values?.isEmpty()!!) {
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_nickname), null, contact.nickName?.values!![0])
|
||||
}
|
||||
|
||||
// ADR
|
||||
for (labeledAddress in contact.addresses) {
|
||||
val types = labeledAddress.property.types
|
||||
val type = if (types.size > 0) types[0].value else null
|
||||
addInfoItem(view.context, mainCard, getString(R.string.journal_item_address), type, labeledAddress.property.label)
|
||||
}
|
||||
|
||||
// NOTE
|
||||
if (contact.note != null) {
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_note), null, contact.note)
|
||||
}
|
||||
|
||||
// URL
|
||||
for (labeledUrl in contact.urls) {
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_website), null, labeledUrl.property.value)
|
||||
}
|
||||
|
||||
// ANNIVERSARY
|
||||
if (contact.anniversary != null) {
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_anniversary), null, getDisplayedDate(contact.anniversary?.date, contact.anniversary?.partialDate))
|
||||
}
|
||||
// BDAY
|
||||
if (contact.birthDay != null) {
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_birthday), null, getDisplayedDate(contact.birthDay?.date, contact.birthDay?.partialDate))
|
||||
}
|
||||
|
||||
// RELATED
|
||||
for (related in contact.relations) {
|
||||
val types = related.types
|
||||
val type = if (types.size > 0) types[0].value else null
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_relation), type, related.text)
|
||||
}
|
||||
|
||||
// PHOTO
|
||||
// if (contact.photo != null)
|
||||
}
|
||||
|
||||
private fun getDisplayedDate(date: Date?, partialDate: PartialDate?): String? {
|
||||
if (date != null) {
|
||||
val epochDate = date.time
|
||||
return getDisplayedDatetime(epochDate, epochDate, true, context)
|
||||
} else if (partialDate != null){
|
||||
val formatter = SimpleDateFormat("d MMMM", Locale.getDefault())
|
||||
val calendar = GregorianCalendar()
|
||||
calendar.set(Calendar.DAY_OF_MONTH, partialDate.date!!)
|
||||
calendar.set(Calendar.MONTH, partialDate.month!! - 1)
|
||||
return formatter.format(calendar.time)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun addInfoItem(context: Context, parent: ViewGroup, type: String, label: String?, value: String?): View {
|
||||
val layout = parent.findViewById<View>(R.id.container) as ViewGroup
|
||||
val infoItem = LayoutInflater.from(context).inflate(R.layout.contact_info_item, layout, false)
|
||||
layout.addView(infoItem)
|
||||
setTextViewText(infoItem, R.id.type, type)
|
||||
setTextViewText(infoItem, R.id.title, label)
|
||||
setTextViewText(infoItem, R.id.content, value)
|
||||
parent.visibility = View.VISIBLE
|
||||
|
||||
return infoItem
|
||||
}
|
||||
|
||||
private fun setTextViewText(parent: View, id: Int, text: String?) {
|
||||
val tv = parent.findViewById<View>(id) as TextView
|
||||
if (text == null) {
|
||||
tv.visibility = View.GONE
|
||||
} else {
|
||||
tv.text = text
|
||||
}
|
||||
}
|
||||
|
||||
fun getDisplayedDatetime(startMillis: Long, endMillis: Long, allDay: Boolean, context: Context?): String? {
|
||||
// Configure date/time formatting.
|
||||
val flagsDate = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY
|
||||
var flagsTime = DateUtils.FORMAT_SHOW_TIME
|
||||
if (DateFormat.is24HourFormat(context)) {
|
||||
flagsTime = flagsTime or DateUtils.FORMAT_24HOUR
|
||||
}
|
||||
|
||||
val datetimeString: String
|
||||
if (allDay) {
|
||||
// For multi-day allday events or single-day all-day events that are not
|
||||
// today or tomorrow, use framework formatter.
|
||||
|
||||
// We need to remove 24hrs because full day events are from the start of a day until the start of the next
|
||||
var adjustedEnd = endMillis - 24 * 60 * 60 * 1000;
|
||||
if (adjustedEnd < startMillis) {
|
||||
adjustedEnd = startMillis;
|
||||
}
|
||||
val f = Formatter(StringBuilder(50), Locale.getDefault())
|
||||
datetimeString = DateUtils.formatDateRange(context, f, startMillis,
|
||||
adjustedEnd, flagsDate).toString()
|
||||
} else {
|
||||
// For multiday events, shorten day/month names.
|
||||
// Example format: "Fri Apr 6, 5:00pm - Sun, Apr 8, 6:00pm"
|
||||
val flagsDatetime = flagsDate or flagsTime or DateUtils.FORMAT_ABBREV_MONTH or
|
||||
DateUtils.FORMAT_ABBREV_WEEKDAY
|
||||
datetimeString = DateUtils.formatDateRange(context, startMillis, endMillis,
|
||||
flagsDatetime)
|
||||
}
|
||||
return datetimeString
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
package com.etesync.syncadapter.ui.etebase
|
||||
|
||||
import android.app.Dialog
|
||||
import android.app.ProgressDialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.etebase.client.CollectionAccessLevel
|
||||
import com.etebase.client.Utils
|
||||
import com.etebase.client.exceptions.EtebaseException
|
||||
import com.etebase.client.exceptions.NotFoundException
|
||||
import com.etesync.syncadapter.CachedCollection
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.resource.LocalCalendar
|
||||
import com.etesync.syncadapter.syncadapter.requestSync
|
||||
import com.etesync.syncadapter.ui.BaseActivity
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
|
||||
class CollectionMembersFragment : Fragment() {
|
||||
private val model: AccountViewModel by activityViewModels()
|
||||
private val collectionModel: CollectionViewModel by activityViewModels()
|
||||
private var isAdmin: Boolean = false
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val ret = if (collectionModel.value!!.col.accessLevel == CollectionAccessLevel.Admin) {
|
||||
isAdmin = true
|
||||
inflater.inflate(R.layout.etebase_view_collection_members, container, false)
|
||||
} else {
|
||||
inflater.inflate(R.layout.etebase_view_collection_members_no_access, container, false)
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
collectionModel.observe(this) {
|
||||
(activity as? BaseActivity?)?.supportActionBar?.setTitle(R.string.collection_members_title)
|
||||
if (container != null) {
|
||||
initUi(inflater, ret, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
private fun initUi(inflater: LayoutInflater, v: View, cachedCollection: CachedCollection) {
|
||||
val meta = cachedCollection.meta
|
||||
val colorSquare = v.findViewById<View>(R.id.color)
|
||||
val color = LocalCalendar.parseColor(meta.color)
|
||||
when (meta.collectionType) {
|
||||
Constants.ETEBASE_TYPE_CALENDAR -> {
|
||||
colorSquare.setBackgroundColor(color)
|
||||
}
|
||||
Constants.ETEBASE_TYPE_TASKS -> {
|
||||
colorSquare.setBackgroundColor(color)
|
||||
}
|
||||
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
|
||||
colorSquare.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
val title = v.findViewById<View>(R.id.display_name) as TextView
|
||||
title.text = meta.name
|
||||
|
||||
val desc = v.findViewById<View>(R.id.description) as TextView
|
||||
desc.text = meta.description
|
||||
|
||||
if (isAdmin) {
|
||||
v.findViewById<View>(R.id.add_member).setOnClickListener {
|
||||
addMemberClicked()
|
||||
}
|
||||
} else {
|
||||
v.findViewById<Button>(R.id.leave).setOnClickListener {
|
||||
doAsync {
|
||||
val membersManager = model.value!!.colMgr.getMemberManager(cachedCollection.col)
|
||||
membersManager.leave()
|
||||
val applicationContext = activity?.applicationContext
|
||||
if (applicationContext != null) {
|
||||
requestSync(applicationContext, model.value!!.account)
|
||||
}
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v.findViewById<View>(R.id.progressBar).visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun addMemberClicked() {
|
||||
val view = View.inflate(requireContext(), R.layout.add_member_fragment, null)
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.collection_members_add)
|
||||
.setIcon(R.drawable.ic_account_add_dark)
|
||||
.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
val username = view.findViewById<EditText>(R.id.username).text.toString()
|
||||
val readOnly = view.findViewById<CheckBox>(R.id.read_only).isChecked
|
||||
|
||||
val frag = AddMemberFragment(model.value!!, collectionModel.value!!, username, if (readOnly) CollectionAccessLevel.ReadOnly else CollectionAccessLevel.ReadWrite)
|
||||
frag.show(childFragmentManager, null)
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
dialog.setView(view)
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
class AddMemberFragment(private val accountHolder: AccountHolder, private val cachedCollection: CachedCollection, private val username: String, private val accessLevel: CollectionAccessLevel) : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val progress = ProgressDialog(context)
|
||||
progress.setTitle(R.string.collection_members_adding)
|
||||
progress.setMessage(getString(R.string.please_wait))
|
||||
progress.isIndeterminate = true
|
||||
progress.setCanceledOnTouchOutside(false)
|
||||
isCancelable = false
|
||||
|
||||
doAsync {
|
||||
val invitationManager = accountHolder.etebase.invitationManager
|
||||
try {
|
||||
val profile = invitationManager.fetchUserProfile(username)
|
||||
val fingerprint = Utils.prettyFingerprint(profile.pubkey)
|
||||
uiThread {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.fingerprint_alertdialog, null)
|
||||
(view.findViewById<View>(R.id.body) as TextView).text = getString(R.string.trust_fingerprint_body, username)
|
||||
(view.findViewById<View>(R.id.fingerprint) as TextView).text = fingerprint
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setIcon(R.drawable.ic_fingerprint_dark)
|
||||
.setTitle(R.string.trust_fingerprint_title)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
doAsync {
|
||||
try {
|
||||
invitationManager.invite(cachedCollection.col, username, profile.pubkey, accessLevel)
|
||||
uiThread { dismiss() }
|
||||
} catch (e: EtebaseException) {
|
||||
uiThread { handleError(e.localizedMessage) }
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }.show()
|
||||
}
|
||||
} catch (e: NotFoundException) {
|
||||
uiThread { handleError(getString(R.string.collection_members_error_user_not_found, username)) }
|
||||
} catch (e: EtebaseException) {
|
||||
uiThread { handleError(e.localizedMessage) }
|
||||
}
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
private fun handleError(message: String) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setIcon(R.drawable.ic_error_dark)
|
||||
.setTitle(R.string.collection_members_add_error)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.yes) { _, _ -> }.show()
|
||||
dismiss()
|
||||
}
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
package com.etesync.syncadapter.ui.etebase
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.ListFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.observe
|
||||
import com.etebase.client.CollectionAccessLevel
|
||||
import com.etebase.client.CollectionMember
|
||||
import com.etebase.client.FetchOptions
|
||||
import com.etesync.syncadapter.CachedCollection
|
||||
import com.etesync.syncadapter.R
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.util.*
|
||||
import java.util.concurrent.Future
|
||||
|
||||
class CollectionMembersListFragment : ListFragment(), AdapterView.OnItemClickListener {
|
||||
private val model: AccountViewModel by activityViewModels()
|
||||
private val collectionModel: CollectionViewModel by activityViewModels()
|
||||
private val membersModel: CollectionMembersViewModel by viewModels()
|
||||
|
||||
private var emptyTextView: TextView? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.collection_members_list, container, false)
|
||||
|
||||
//This is instead of setEmptyText() function because of Google bug
|
||||
//See: https://code.google.com/p/android/issues/detail?id=21742
|
||||
emptyTextView = view.findViewById<TextView>(android.R.id.empty)
|
||||
return view
|
||||
}
|
||||
|
||||
private fun setListAdapterMembers(members: List<CollectionMember>) {
|
||||
val context = context
|
||||
if (context != null) {
|
||||
val listAdapter = MembersListAdapter(context)
|
||||
setListAdapter(listAdapter)
|
||||
|
||||
listAdapter.addAll(members)
|
||||
|
||||
emptyTextView!!.setText(R.string.collection_members_list_empty)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
model.observe(this) {
|
||||
collectionModel.observe(this) { cachedCollection ->
|
||||
membersModel.loadMembers(it, cachedCollection)
|
||||
}
|
||||
}
|
||||
|
||||
membersModel.observe(this) {
|
||||
setListAdapterMembers(it)
|
||||
}
|
||||
|
||||
listView.onItemClickListener = this
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
|
||||
membersModel.cancelLoad()
|
||||
}
|
||||
|
||||
override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
|
||||
val member = listAdapter?.getItem(position) as CollectionMember
|
||||
|
||||
if (member.accessLevel == CollectionAccessLevel.Admin) {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setIcon(R.drawable.ic_error_dark)
|
||||
.setTitle(R.string.collection_members_remove_title)
|
||||
.setMessage(R.string.collection_members_remove_admin)
|
||||
.setNegativeButton(android.R.string.ok) { _, _ -> }.show()
|
||||
return
|
||||
}
|
||||
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setIcon(R.drawable.ic_info_dark)
|
||||
.setTitle(R.string.collection_members_remove_title)
|
||||
.setMessage(getString(R.string.collection_members_remove, member.username))
|
||||
.setPositiveButton(android.R.string.yes) { dialog, which ->
|
||||
membersModel.removeMember(model.value!!, collectionModel.value!!, member.username)
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { dialog, which -> }.show()
|
||||
}
|
||||
|
||||
internal inner class MembersListAdapter(context: Context) : ArrayAdapter<CollectionMember>(context, R.layout.collection_members_list_item) {
|
||||
|
||||
override fun getView(position: Int, _v: View?, parent: ViewGroup): View {
|
||||
var v = _v
|
||||
if (v == null)
|
||||
v = LayoutInflater.from(context).inflate(R.layout.collection_members_list_item, parent, false)
|
||||
|
||||
val member = getItem(position)
|
||||
|
||||
val tv = v!!.findViewById<View>(R.id.title) as TextView
|
||||
tv.text = member!!.username
|
||||
|
||||
// FIXME: Also mark admins
|
||||
val readOnly = v.findViewById<View>(R.id.read_only)
|
||||
readOnly.visibility = if (member.accessLevel == CollectionAccessLevel.ReadOnly) View.VISIBLE else View.GONE
|
||||
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CollectionMembersViewModel : ViewModel() {
|
||||
private val members = MutableLiveData<List<CollectionMember>>()
|
||||
private var asyncTask: Future<Unit>? = null
|
||||
|
||||
fun loadMembers(accountCollectionHolder: AccountHolder, cachedCollection: CachedCollection) {
|
||||
asyncTask = doAsync {
|
||||
val ret = LinkedList<CollectionMember>()
|
||||
val col = cachedCollection.col
|
||||
val memberManager = accountCollectionHolder.colMgr.getMemberManager(col)
|
||||
var iterator: String? = null
|
||||
var done = false
|
||||
while (!done) {
|
||||
val chunk = memberManager.list(FetchOptions().iterator(iterator).limit(30))
|
||||
iterator = chunk.stoken
|
||||
done = chunk.isDone
|
||||
|
||||
ret.addAll(chunk.data)
|
||||
}
|
||||
|
||||
uiThread {
|
||||
members.value = ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMember(accountCollectionHolder: AccountHolder, cachedCollection: CachedCollection, username: String) {
|
||||
doAsync {
|
||||
val col = cachedCollection.col
|
||||
val memberManager = accountCollectionHolder.colMgr.getMemberManager(col)
|
||||
memberManager.remove(username)
|
||||
val ret = members.value!!.filter { it.username != username }
|
||||
|
||||
uiThread {
|
||||
members.value = ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelLoad() {
|
||||
asyncTask?.cancel(true)
|
||||
}
|
||||
|
||||
fun observe(owner: LifecycleOwner, observer: (List<CollectionMember>) -> Unit) =
|
||||
members.observe(owner, observer)
|
||||
}
|
@ -0,0 +1,254 @@
|
||||
package com.etesync.syncadapter.ui.etebase
|
||||
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.etebase.client.Collection
|
||||
import com.etebase.client.exceptions.EtebaseException
|
||||
import com.etesync.syncadapter.CachedCollection
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.resource.LocalCalendar
|
||||
import com.etesync.syncadapter.syncadapter.requestSync
|
||||
import com.etesync.syncadapter.ui.BaseActivity
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import yuku.ambilwarna.AmbilWarnaDialog
|
||||
|
||||
class EditCollectionFragment(private val cachedCollection: CachedCollection, private val isCreating: Boolean = false) : Fragment() {
|
||||
private val model: AccountViewModel by activityViewModels()
|
||||
private val collectionModel: CollectionViewModel by activityViewModels()
|
||||
private val itemsModel: ItemsViewModel by activityViewModels()
|
||||
private val loadingModel: LoadingViewModel by viewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val ret = inflater.inflate(R.layout.activity_create_collection, container, false)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
updateTitle()
|
||||
if (container != null) {
|
||||
initUi(inflater, ret)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
fun updateTitle() {
|
||||
cachedCollection.let {
|
||||
var titleId: Int = R.string.create_calendar
|
||||
if (isCreating) {
|
||||
when (cachedCollection.meta.collectionType) {
|
||||
Constants.ETEBASE_TYPE_CALENDAR -> {
|
||||
titleId = R.string.create_calendar
|
||||
}
|
||||
Constants.ETEBASE_TYPE_TASKS -> {
|
||||
titleId = R.string.create_tasklist
|
||||
}
|
||||
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
|
||||
titleId = R.string.create_addressbook
|
||||
}
|
||||
}
|
||||
} else {
|
||||
titleId = R.string.edit_collection
|
||||
}
|
||||
(activity as? BaseActivity?)?.supportActionBar?.setTitle(titleId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initUi(inflater: LayoutInflater, v: View) {
|
||||
val title = v.findViewById<EditText>(R.id.display_name)
|
||||
val desc = v.findViewById<EditText>(R.id.description)
|
||||
|
||||
val meta = cachedCollection.meta
|
||||
|
||||
title.setText(meta.name)
|
||||
desc.setText(meta.description)
|
||||
|
||||
val colorSquare = v.findViewById<View>(R.id.color)
|
||||
when (cachedCollection.meta.collectionType) {
|
||||
Constants.ETEBASE_TYPE_CALENDAR -> {
|
||||
title.setHint(R.string.create_calendar_display_name_hint)
|
||||
|
||||
val color = LocalCalendar.parseColor(meta.color)
|
||||
colorSquare.setBackgroundColor(color)
|
||||
colorSquare.setOnClickListener {
|
||||
AmbilWarnaDialog(context, (colorSquare.background as ColorDrawable).color, true, object : AmbilWarnaDialog.OnAmbilWarnaListener {
|
||||
override fun onCancel(dialog: AmbilWarnaDialog) {}
|
||||
|
||||
override fun onOk(dialog: AmbilWarnaDialog, color: Int) {
|
||||
colorSquare.setBackgroundColor(color)
|
||||
}
|
||||
}).show()
|
||||
}
|
||||
}
|
||||
Constants.ETEBASE_TYPE_TASKS -> {
|
||||
title.setHint(R.string.create_tasklist_display_name_hint)
|
||||
|
||||
val color = LocalCalendar.parseColor(meta.color)
|
||||
colorSquare.setBackgroundColor(color)
|
||||
colorSquare.setOnClickListener {
|
||||
AmbilWarnaDialog(context, (colorSquare.background as ColorDrawable).color, true, object : AmbilWarnaDialog.OnAmbilWarnaListener {
|
||||
override fun onCancel(dialog: AmbilWarnaDialog) {}
|
||||
|
||||
override fun onOk(dialog: AmbilWarnaDialog, color: Int) {
|
||||
colorSquare.setBackgroundColor(color)
|
||||
}
|
||||
}).show()
|
||||
}
|
||||
}
|
||||
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
|
||||
title.setHint(R.string.create_addressbook_display_name_hint)
|
||||
|
||||
val colorGroup = v.findViewById<View>(R.id.color_group)
|
||||
colorGroup.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.fragment_edit_collection, menu)
|
||||
if (isCreating) {
|
||||
menu.findItem(R.id.on_delete).setVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.on_delete -> {
|
||||
deleteColection()
|
||||
}
|
||||
R.id.on_save -> {
|
||||
saveCollection()
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun deleteColection() {
|
||||
val meta = cachedCollection.meta
|
||||
val name = meta.name
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.delete_collection_confirm_title)
|
||||
.setMessage(getString(R.string.delete_collection_confirm_warning, name))
|
||||
.setPositiveButton(android.R.string.yes) { dialog, _ ->
|
||||
doDeleteCollection()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun doDeleteCollection() {
|
||||
loadingModel.setLoading(true)
|
||||
doAsync {
|
||||
try {
|
||||
val col = cachedCollection.col
|
||||
col.delete()
|
||||
uploadCollection(col)
|
||||
val applicationContext = activity?.applicationContext
|
||||
if (applicationContext != null) {
|
||||
requestSync(applicationContext, model.value!!.account)
|
||||
}
|
||||
activity?.finish()
|
||||
} catch (e: EtebaseException) {
|
||||
uiThread {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setIcon(R.drawable.ic_info_dark)
|
||||
.setTitle(R.string.exception)
|
||||
.setMessage(e.localizedMessage)
|
||||
.setPositiveButton(android.R.string.yes) { _, _ -> }.show()
|
||||
}
|
||||
} finally {
|
||||
uiThread {
|
||||
loadingModel.setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveCollection() {
|
||||
var ok = true
|
||||
|
||||
val meta = cachedCollection.meta
|
||||
val v = requireView()
|
||||
|
||||
var edit = v.findViewById<EditText>(R.id.display_name)
|
||||
meta.name = edit.text.toString()
|
||||
if (TextUtils.isEmpty(meta.name)) {
|
||||
edit.error = getString(R.string.create_collection_display_name_required)
|
||||
ok = false
|
||||
}
|
||||
|
||||
edit = v.findViewById<EditText>(R.id.description)
|
||||
meta.description = StringUtils.trimToNull(edit.text.toString())
|
||||
|
||||
if (ok) {
|
||||
when (meta.collectionType) {
|
||||
Constants.ETEBASE_TYPE_CALENDAR, Constants.ETEBASE_TYPE_TASKS -> {
|
||||
val view = v.findViewById<View>(R.id.color)
|
||||
val color = (view.background as ColorDrawable).color
|
||||
meta.color = String.format("#%06X", 0xFFFFFF and color)
|
||||
}
|
||||
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
|
||||
}
|
||||
}
|
||||
|
||||
loadingModel.setLoading(true)
|
||||
doAsync {
|
||||
try {
|
||||
val col = cachedCollection.col
|
||||
col.meta = meta
|
||||
uploadCollection(col)
|
||||
val applicationContext = activity?.applicationContext
|
||||
if (applicationContext != null) {
|
||||
requestSync(applicationContext, model.value!!.account)
|
||||
}
|
||||
if (isCreating) {
|
||||
// Load the items since we just created it
|
||||
itemsModel.loadItems(model.value!!, cachedCollection)
|
||||
parentFragmentManager.commit {
|
||||
replace(R.id.fragment_container, ViewCollectionFragment())
|
||||
}
|
||||
} else {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
} catch (e: EtebaseException) {
|
||||
uiThread {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setIcon(R.drawable.ic_info_dark)
|
||||
.setTitle(R.string.exception)
|
||||
.setMessage(e.localizedMessage)
|
||||
.setPositiveButton(android.R.string.yes) { _, _ -> }.show()
|
||||
}
|
||||
} finally {
|
||||
uiThread {
|
||||
loadingModel.setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun uploadCollection(col: Collection) {
|
||||
val accountHolder = model.value!!
|
||||
val etebaseLocalCache = accountHolder.etebaseLocalCache
|
||||
val colMgr = accountHolder.colMgr
|
||||
colMgr.upload(col)
|
||||
synchronized(etebaseLocalCache) {
|
||||
etebaseLocalCache.collectionSet(colMgr, col)
|
||||
}
|
||||
collectionModel.loadCollection(model.value!!, col.uid)
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package com.etesync.syncadapter.ui.etebase
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.commit
|
||||
import com.etesync.syncadapter.CachedCollection
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.ui.BaseActivity
|
||||
import com.etesync.syncadapter.ui.importlocal.ImportFragment
|
||||
import com.etesync.syncadapter.ui.importlocal.LocalCalendarImportFragment
|
||||
import com.etesync.syncadapter.ui.importlocal.LocalContactImportFragment
|
||||
|
||||
class ImportCollectionFragment : Fragment() {
|
||||
private val model: AccountViewModel by activityViewModels()
|
||||
private val collectionModel: CollectionViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val ret = inflater.inflate(R.layout.import_actions_list, container, false)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
collectionModel.observe(this) {
|
||||
(activity as? BaseActivity?)?.supportActionBar?.setTitle(R.string.import_dialog_title)
|
||||
if (container != null) {
|
||||
initUi(inflater, ret, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
private fun initUi(inflater: LayoutInflater, v: View, cachedCollection: CachedCollection) {
|
||||
val accountHolder = model.value!!
|
||||
|
||||
var card = v.findViewById<View>(R.id.import_file)
|
||||
var img = card.findViewById<View>(R.id.action_icon) as ImageView
|
||||
var text = card.findViewById<View>(R.id.action_text) as TextView
|
||||
img.setImageResource(R.drawable.ic_file_white)
|
||||
text.setText(R.string.import_button_file)
|
||||
card.setOnClickListener {
|
||||
parentFragmentManager.commit {
|
||||
add(ImportFragment.newInstance(accountHolder.account, cachedCollection), null)
|
||||
}
|
||||
}
|
||||
|
||||
card = v.findViewById(R.id.import_account)
|
||||
img = card.findViewById<View>(R.id.action_icon) as ImageView
|
||||
text = card.findViewById<View>(R.id.action_text) as TextView
|
||||
img.setImageResource(R.drawable.ic_account_circle_white)
|
||||
text.setText(R.string.import_button_local)
|
||||
card.setOnClickListener {
|
||||
if (cachedCollection.meta.collectionType == Constants.ETEBASE_TYPE_CALENDAR) {
|
||||
parentFragmentManager.commit {
|
||||
replace(R.id.fragment_container, LocalCalendarImportFragment(accountHolder.account, cachedCollection.col.uid))
|
||||
addToBackStack(null)
|
||||
}
|
||||
} else if (cachedCollection.meta.collectionType == Constants.ETEBASE_TYPE_ADDRESS_BOOK) {
|
||||
parentFragmentManager.commit {
|
||||
replace(R.id.fragment_container, LocalContactImportFragment(accountHolder.account, cachedCollection.col.uid))
|
||||
addToBackStack(null)
|
||||
}
|
||||
}
|
||||
// FIXME: should be in the fragments once we kill legacy
|
||||
(activity as? BaseActivity?)?.supportActionBar?.setTitle(R.string.import_select_account)
|
||||
}
|
||||
|
||||
if (collectionModel.value!!.meta.collectionType == Constants.ETEBASE_TYPE_TASKS) {
|
||||
card.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package com.etesync.syncadapter.ui.etebase
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.fragment.app.commit
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.ui.BaseActivity
|
||||
|
||||
class InvitationsActivity : BaseActivity() {
|
||||
private lateinit var account: Account
|
||||
private val model: AccountViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
account = intent.extras!!.getParcelable(EXTRA_ACCOUNT)!!
|
||||
|
||||
setContentView(R.layout.etebase_collection_activity)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
model.loadAccount(this, account)
|
||||
title = getString(R.string.invitations_title)
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.fragment_container, InvitationsListFragment())
|
||||
}
|
||||
}
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val EXTRA_ACCOUNT = "account"
|
||||
|
||||
fun newIntent(context: Context, account: Account): Intent {
|
||||
val intent = Intent(context, InvitationsActivity::class.java)
|
||||
intent.putExtra(EXTRA_ACCOUNT, account)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
package com.etesync.syncadapter.ui.etebase
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.ListFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.observe
|
||||
import com.etebase.client.CollectionAccessLevel
|
||||
import com.etebase.client.FetchOptions
|
||||
import com.etebase.client.SignedInvitation
|
||||
import com.etebase.client.Utils
|
||||
import com.etesync.syncadapter.R
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.util.*
|
||||
import java.util.concurrent.Future
|
||||
|
||||
|
||||
class InvitationsListFragment : ListFragment(), AdapterView.OnItemClickListener {
|
||||
private val model: AccountViewModel by activityViewModels()
|
||||
private val invitationsModel: InvitationsViewModel by viewModels()
|
||||
|
||||
private var emptyTextView: TextView? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.invitations_list, container, false)
|
||||
|
||||
//This is instead of setEmptyText() function because of Google bug
|
||||
//See: https://code.google.com/p/android/issues/detail?id=21742
|
||||
emptyTextView = view.findViewById<TextView>(android.R.id.empty)
|
||||
return view
|
||||
}
|
||||
|
||||
private fun setListAdapterInvitations(invitations: List<SignedInvitation>) {
|
||||
val context = context
|
||||
if (context != null) {
|
||||
val listAdapter = InvitationsListAdapter(context)
|
||||
setListAdapter(listAdapter)
|
||||
|
||||
listAdapter.addAll(invitations)
|
||||
|
||||
emptyTextView!!.setText(R.string.invitations_list_empty)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
model.observe(this) {
|
||||
invitationsModel.loadInvitations(it)
|
||||
}
|
||||
|
||||
invitationsModel.observe(this) {
|
||||
setListAdapterInvitations(it)
|
||||
}
|
||||
|
||||
listView.onItemClickListener = this
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
|
||||
invitationsModel.cancelLoad()
|
||||
}
|
||||
|
||||
override fun onItemClick(parent: AdapterView<*>, view_: View, position: Int, id: Long) {
|
||||
val invitation = listAdapter?.getItem(position) as SignedInvitation
|
||||
val fingerprint = Utils.prettyFingerprint(invitation.fromPubkey)
|
||||
val view = layoutInflater.inflate(R.layout.invitation_alert_dialog, null)
|
||||
view.findViewById<TextView>(R.id.body).text = getString(R.string.invitations_accept_reject_dialog)
|
||||
view.findViewById<TextView>(R.id.fingerprint).text = fingerprint
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.invitations_title)
|
||||
.setIcon(R.drawable.ic_email_black)
|
||||
.setView(view)
|
||||
.setNegativeButton(R.string.invitations_reject) { dialogInterface, i ->
|
||||
invitationsModel.reject(model.value!!, invitation)
|
||||
}
|
||||
.setPositiveButton(R.string.invitations_accept) { dialogInterface, i ->
|
||||
invitationsModel.accept(model.value!!, invitation)
|
||||
}
|
||||
.show()
|
||||
return
|
||||
}
|
||||
|
||||
internal inner class InvitationsListAdapter(context: Context) : ArrayAdapter<SignedInvitation>(context, R.layout.invitations_list_item) {
|
||||
|
||||
override fun getView(position: Int, _v: View?, parent: ViewGroup): View {
|
||||
var v = _v
|
||||
if (v == null)
|
||||
v = LayoutInflater.from(context).inflate(R.layout.invitations_list_item, parent, false)
|
||||
|
||||
val invitation = getItem(position)!!
|
||||
|
||||
val tv = v!!.findViewById<View>(R.id.title) as TextView
|
||||
// FIXME: Should have a sensible string here
|
||||
tv.text = "Invitation ${position}"
|
||||
|
||||
// FIXME: Also mark admins
|
||||
val readOnly = v.findViewById<View>(R.id.read_only)
|
||||
readOnly.visibility = if (invitation.accessLevel == CollectionAccessLevel.ReadOnly) View.VISIBLE else View.GONE
|
||||
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InvitationsViewModel : ViewModel() {
|
||||
private val invitations = MutableLiveData<List<SignedInvitation>>()
|
||||
private var asyncTask: Future<Unit>? = null
|
||||
|
||||
fun loadInvitations(accountCollectionHolder: AccountHolder) {
|
||||
asyncTask = doAsync {
|
||||
val ret = LinkedList<SignedInvitation>()
|
||||
val invitationManager = accountCollectionHolder.etebase.invitationManager
|
||||
var iterator: String? = null
|
||||
var done = false
|
||||
while (!done) {
|
||||
val chunk = invitationManager.listIncoming(FetchOptions().iterator(iterator).limit(30))
|
||||
iterator = chunk.stoken
|
||||
done = chunk.isDone
|
||||
|
||||
ret.addAll(chunk.data)
|
||||
}
|
||||
|
||||
uiThread {
|
||||
invitations.value = ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun accept(accountCollectionHolder: AccountHolder, invitation: SignedInvitation) {
|
||||
doAsync {
|
||||
val invitationManager = accountCollectionHolder.etebase.invitationManager
|
||||
invitationManager.accept(invitation)
|
||||
val ret = invitations.value!!.filter { it != invitation }
|
||||
|
||||
uiThread {
|
||||
invitations.value = ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reject(accountCollectionHolder: AccountHolder, invitation: SignedInvitation) {
|
||||
doAsync {
|
||||
val invitationManager = accountCollectionHolder.etebase.invitationManager
|
||||
invitationManager.reject(invitation)
|
||||
val ret = invitations.value!!.filter { it != invitation }
|
||||
|
||||
uiThread {
|
||||
invitations.value = ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelLoad() {
|
||||
asyncTask?.cancel(true)
|
||||
}
|
||||
|
||||
fun observe(owner: LifecycleOwner, observer: (List<SignedInvitation>) -> Unit) =
|
||||
invitations.observe(owner, observer)
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
package com.etesync.syncadapter.ui.etebase
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.ListFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.observe
|
||||
import com.etebase.client.FetchOptions
|
||||
import com.etesync.syncadapter.CachedCollection
|
||||
import com.etesync.syncadapter.CachedItem
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.ui.etebase.ListEntriesFragment.Companion.setItemView
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.util.*
|
||||
import java.util.concurrent.Future
|
||||
|
||||
|
||||
class ItemRevisionsListFragment(private val cachedCollection: CachedCollection, private val cachedItem: CachedItem) : ListFragment(), AdapterView.OnItemClickListener {
|
||||
private val model: AccountViewModel by activityViewModels()
|
||||
private val revisionsModel: RevisionsViewModel by viewModels()
|
||||
private var state: Parcelable? = null
|
||||
|
||||
private var emptyTextView: TextView? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.journal_viewer_list, container, false)
|
||||
|
||||
//This is instead of setEmptyText() function because of Google bug
|
||||
//See: https://code.google.com/p/android/issues/detail?id=21742
|
||||
emptyTextView = view.findViewById<View>(android.R.id.empty) as TextView
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
var restored = false
|
||||
|
||||
revisionsModel.loadRevisions(model.value!!, cachedCollection, cachedItem)
|
||||
revisionsModel.observe(this) {
|
||||
val entries = it.sortedByDescending { item ->
|
||||
item.meta.mtime ?: 0
|
||||
}
|
||||
val listAdapter = EntriesListAdapter(requireContext(), cachedCollection)
|
||||
setListAdapter(listAdapter)
|
||||
|
||||
listAdapter.addAll(entries)
|
||||
|
||||
if(!restored && (state != null)) {
|
||||
listView.onRestoreInstanceState(state)
|
||||
restored = true
|
||||
}
|
||||
|
||||
emptyTextView!!.text = getString(R.string.journal_entries_list_empty)
|
||||
}
|
||||
|
||||
listView.onItemClickListener = this
|
||||
}
|
||||
override fun onPause() {
|
||||
state = listView.onSaveInstanceState()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
revisionsModel.cancelLoad()
|
||||
}
|
||||
|
||||
override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
|
||||
val item = listAdapter?.getItem(position) as CachedItem
|
||||
activity?.supportFragmentManager?.commit {
|
||||
replace(R.id.fragment_container, CollectionItemFragment(item))
|
||||
addToBackStack(null)
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class EntriesListAdapter(context: Context, val cachedCollection: CachedCollection) : ArrayAdapter<CachedItem>(context, R.layout.journal_viewer_list_item) {
|
||||
|
||||
override fun getView(position: Int, _v: View?, parent: ViewGroup): View {
|
||||
var v = _v
|
||||
if (v == null)
|
||||
v = LayoutInflater.from(context).inflate(R.layout.journal_viewer_list_item, parent, false)!!
|
||||
|
||||
val item = getItem(position)
|
||||
|
||||
setItemView(v, cachedCollection.meta.collectionType, item)
|
||||
|
||||
/* FIXME: handle entry error:
|
||||
val entryError = data.select(EntryErrorEntity::class.java).where(EntryErrorEntity.ENTRY.eq(entryEntity)).limit(1).get().firstOrNull()
|
||||
if (entryError != null) {
|
||||
val errorIcon = v.findViewById<View>(R.id.error) as ImageView
|
||||
errorIcon.visibility = View.VISIBLE
|
||||
}
|
||||
*/
|
||||
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RevisionsViewModel : ViewModel() {
|
||||
private val revisions = MutableLiveData<List<CachedItem>>()
|
||||
private var asyncTask: Future<Unit>? = null
|
||||
|
||||
fun loadRevisions(accountCollectionHolder: AccountHolder, cachedCollection: CachedCollection, cachedItem: CachedItem) {
|
||||
asyncTask = doAsync {
|
||||
val ret = LinkedList<CachedItem>()
|
||||
val col = cachedCollection.col
|
||||
val itemManager = accountCollectionHolder.colMgr.getItemManager(col)
|
||||
var iterator: String? = null
|
||||
var done = false
|
||||
while (!done) {
|
||||
val chunk = itemManager.itemRevisions(cachedItem.item, FetchOptions().iterator(iterator).limit(30))
|
||||
iterator = chunk.iterator
|
||||
done = chunk.isDone
|
||||
|
||||
ret.addAll(chunk.data.map { CachedItem(it, it.meta, it.contentString) })
|
||||
}
|
||||
|
||||
uiThread {
|
||||
revisions.value = ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelLoad() {
|
||||
asyncTask?.cancel(true)
|
||||
}
|
||||
|
||||
fun observe(owner: LifecycleOwner, observer: (List<CachedItem>) -> Unit) =
|
||||
revisions.observe(owner, observer)
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
package com.etesync.syncadapter.ui.etebase
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.ListFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.commit
|
||||
import com.etesync.syncadapter.CachedCollection
|
||||
import com.etesync.syncadapter.CachedItem
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
|
||||
class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener {
|
||||
private val collectionModel: CollectionViewModel by activityViewModels()
|
||||
private val itemsModel: ItemsViewModel by activityViewModels()
|
||||
private var state: Parcelable? = null
|
||||
|
||||
private var emptyTextView: TextView? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.journal_viewer_list, container, false)
|
||||
|
||||
//This is instead of setEmptyText() function because of Google bug
|
||||
//See: https://code.google.com/p/android/issues/detail?id=21742
|
||||
emptyTextView = view.findViewById<View>(android.R.id.empty) as TextView
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
var restored = false
|
||||
|
||||
collectionModel.observe(this) { col ->
|
||||
itemsModel.observe(this) {
|
||||
val entries = it.sortedByDescending { item ->
|
||||
item.meta.mtime ?: 0
|
||||
}
|
||||
val listAdapter = EntriesListAdapter(requireContext(), col)
|
||||
setListAdapter(listAdapter)
|
||||
|
||||
listAdapter.addAll(entries)
|
||||
|
||||
if(!restored && (state != null)) {
|
||||
listView.onRestoreInstanceState(state)
|
||||
restored = true
|
||||
}
|
||||
|
||||
emptyTextView!!.text = getString(R.string.journal_entries_list_empty)
|
||||
}
|
||||
}
|
||||
|
||||
listView.onItemClickListener = this
|
||||
}
|
||||
override fun onPause() {
|
||||
state = listView.onSaveInstanceState()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
|
||||
val item = listAdapter?.getItem(position) as CachedItem
|
||||
activity?.supportFragmentManager?.commit {
|
||||
replace(R.id.fragment_container, CollectionItemFragment(item))
|
||||
addToBackStack(EditCollectionFragment::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class EntriesListAdapter(context: Context, val cachedCollection: CachedCollection) : ArrayAdapter<CachedItem>(context, R.layout.journal_viewer_list_item) {
|
||||
|
||||
override fun getView(position: Int, _v: View?, parent: ViewGroup): View {
|
||||
var v = _v
|
||||
if (v == null)
|
||||
v = LayoutInflater.from(context).inflate(R.layout.journal_viewer_list_item, parent, false)!!
|
||||
|
||||
val item = getItem(position)
|
||||
|
||||
setItemView(v, cachedCollection.meta.collectionType, item)
|
||||
|
||||
/* FIXME: handle entry error:
|
||||
val entryError = data.select(EntryErrorEntity::class.java).where(EntryErrorEntity.ENTRY.eq(entryEntity)).limit(1).get().firstOrNull()
|
||||
if (entryError != null) {
|
||||
val errorIcon = v.findViewById<View>(R.id.error) as ImageView
|
||||
errorIcon.visibility = View.VISIBLE
|
||||
}
|
||||
*/
|
||||
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val dateFormatter = SimpleDateFormat()
|
||||
private fun getLine(content: String?, prefix: String): String? {
|
||||
var content: String? = content ?: return null
|
||||
|
||||
val start = content!!.indexOf(prefix)
|
||||
if (start >= 0) {
|
||||
val end = content.indexOf("\n", start)
|
||||
content = content.substring(start + prefix.length, end)
|
||||
} else {
|
||||
content = null
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
fun setItemView(v: View, collectionType: String, item: CachedItem) {
|
||||
|
||||
var tv = v.findViewById<View>(R.id.title) as TextView
|
||||
|
||||
// FIXME: hacky way to make it show sensible info
|
||||
val prefix: String = when (collectionType) {
|
||||
Constants.ETEBASE_TYPE_CALENDAR, Constants.ETEBASE_TYPE_TASKS -> {
|
||||
"SUMMARY:"
|
||||
}
|
||||
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
|
||||
"FN:"
|
||||
}
|
||||
else -> {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
val fullContent = item.content
|
||||
var content = getLine(fullContent, prefix)
|
||||
content = content ?: "Not found"
|
||||
tv.text = content
|
||||
|
||||
tv = v.findViewById<View>(R.id.description) as TextView
|
||||
// FIXME: Don't use a hard-coded string
|
||||
content = "Modified: ${dateFormatter.format(item.meta.mtime ?: 0)}"
|
||||
tv.text = content
|
||||
|
||||
val action = v.findViewById<View>(R.id.action) as ImageView
|
||||
if (item.item.isDeleted) {
|
||||
action.setImageResource(R.drawable.action_delete)
|
||||
} else {
|
||||
action.setImageResource(R.drawable.action_change)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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 com.etesync.syncadapter.ui.etebase
|
||||
|
||||
import android.app.Dialog
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.CheckedTextView
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.observe
|
||||
import com.etebase.client.Account
|
||||
import com.etebase.client.Client
|
||||
import com.etebase.client.User
|
||||
import com.etebase.client.exceptions.EtebaseException
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.HttpClient
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder
|
||||
import com.etesync.syncadapter.ui.setup.CreateAccountFragment
|
||||
import com.etesync.syncadapter.ui.setup.DetectConfigurationFragment
|
||||
import com.etesync.syncadapter.ui.setup.LoginCredentialsFragment
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import net.cachapa.expandablelayout.ExpandableLayout
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.net.URI
|
||||
import java.util.concurrent.Future
|
||||
|
||||
class SignupFragment(private val initialUsername: String?, private val initialPassword: String?) : Fragment() {
|
||||
internal lateinit var editUserName: TextInputLayout
|
||||
internal lateinit var editEmail: TextInputLayout
|
||||
internal lateinit var editPassword: TextInputLayout
|
||||
|
||||
internal lateinit var showAdvanced: CheckedTextView
|
||||
internal lateinit var customServer: TextInputEditText
|
||||
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val v = inflater.inflate(R.layout.signup_fragment, container, false)
|
||||
|
||||
editUserName = v.findViewById(R.id.user_name)
|
||||
editEmail = v.findViewById(R.id.email)
|
||||
editPassword = v.findViewById(R.id.url_password)
|
||||
showAdvanced = v.findViewById(R.id.show_advanced)
|
||||
customServer = v.findViewById(R.id.custom_server)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
editUserName.editText?.setText(initialUsername ?: "")
|
||||
editPassword.editText?.setText(initialPassword ?: "")
|
||||
}
|
||||
|
||||
val login = v.findViewById<Button>(R.id.login)
|
||||
login.setOnClickListener {
|
||||
parentFragmentManager.commit {
|
||||
replace(android.R.id.content, LoginCredentialsFragment(editUserName.editText?.text.toString(), editPassword.editText?.text.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
val createAccount = v.findViewById<Button>(R.id.create_account)
|
||||
createAccount.setOnClickListener {
|
||||
val credentials = validateData()
|
||||
if (credentials != null) {
|
||||
SignupDoFragment(credentials).show(fragmentManager!!, null)
|
||||
}
|
||||
}
|
||||
|
||||
val advancedLayout = v.findViewById<View>(R.id.advanced_layout) as ExpandableLayout
|
||||
|
||||
showAdvanced.setOnClickListener {
|
||||
if (showAdvanced.isChecked) {
|
||||
showAdvanced.isChecked = false
|
||||
advancedLayout.collapse()
|
||||
} else {
|
||||
showAdvanced.isChecked = true
|
||||
advancedLayout.expand()
|
||||
}
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
protected fun validateData(): SignupCredentials? {
|
||||
var valid = true
|
||||
|
||||
val userName = editUserName.editText?.text.toString()
|
||||
if (userName.isEmpty()) {
|
||||
editUserName.error = getString(R.string.login_username_error)
|
||||
valid = false
|
||||
} else {
|
||||
editUserName.error = null
|
||||
}
|
||||
|
||||
val email = editEmail.editText?.text.toString()
|
||||
if (email.isEmpty()) {
|
||||
editEmail.error = getString(R.string.login_email_address_error)
|
||||
valid = false
|
||||
} else {
|
||||
editEmail.error = null
|
||||
}
|
||||
|
||||
val password = editPassword.editText?.text.toString()
|
||||
if (password.isEmpty()) {
|
||||
editPassword.error = getString(R.string.signup_password_restrictions)
|
||||
valid = false
|
||||
} else {
|
||||
editPassword.error = null
|
||||
}
|
||||
|
||||
var uri: URI? = null
|
||||
if (showAdvanced.isChecked) {
|
||||
val server = customServer.text.toString()
|
||||
// If this field is null, just use the default
|
||||
if (!server.isEmpty()) {
|
||||
val url = server.toHttpUrlOrNull()
|
||||
if (url != null) {
|
||||
uri = url.toUri()
|
||||
customServer.error = null
|
||||
} else {
|
||||
customServer.error = getString(R.string.login_custom_server_error)
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (valid) SignupCredentials(uri, userName, email, password) else null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class SignupDoFragment(private val signupCredentials: SignupCredentials) : DialogFragment() {
|
||||
private val model: ConfigurationViewModel by viewModels()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val progress = ProgressDialog(activity)
|
||||
progress.setTitle(R.string.setting_up_encryption)
|
||||
progress.setMessage(getString(R.string.setting_up_encryption_content))
|
||||
progress.isIndeterminate = true
|
||||
progress.setCanceledOnTouchOutside(false)
|
||||
isCancelable = false
|
||||
return progress
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
model.signup(requireContext(), signupCredentials)
|
||||
model.observe(this) {
|
||||
if (it.isFailed) {
|
||||
// no service found: show error message
|
||||
requireFragmentManager().beginTransaction()
|
||||
.add(DetectConfigurationFragment.NothingDetectedFragment.newInstance(it.error!!.localizedMessage), null)
|
||||
.commitAllowingStateLoss()
|
||||
} else {
|
||||
requireFragmentManager().beginTransaction()
|
||||
.replace(android.R.id.content, CreateAccountFragment.newInstance(it))
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigurationViewModel : ViewModel() {
|
||||
private val account = MutableLiveData<BaseConfigurationFinder.Configuration>()
|
||||
private var asyncTask: Future<Unit>? = null
|
||||
|
||||
fun signup(context: Context, credentials: SignupCredentials) {
|
||||
asyncTask = doAsync {
|
||||
val httpClient = HttpClient.Builder(context).build().okHttpClient
|
||||
val uri = credentials.uri ?: URI(Constants.etebaseServiceUrl)
|
||||
var etebaseSession: String? = null
|
||||
var exception: Throwable? = null
|
||||
try {
|
||||
val client = Client.create(httpClient, uri.toString())
|
||||
val user = User(credentials.userName, credentials.email)
|
||||
val etebase = Account.signup(client, user, credentials.password)
|
||||
etebaseSession = etebase.save(null)
|
||||
} catch (e: EtebaseException) {
|
||||
exception = e
|
||||
}
|
||||
|
||||
uiThread {
|
||||
account.value = BaseConfigurationFinder.Configuration(
|
||||
uri,
|
||||
credentials.userName,
|
||||
etebaseSession,
|
||||
null,
|
||||
null,
|
||||
exception
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelLoad() {
|
||||
asyncTask?.cancel(true)
|
||||
}
|
||||
|
||||
fun observe(owner: LifecycleOwner, observer: (BaseConfigurationFinder.Configuration) -> Unit) =
|
||||
account.observe(owner, observer)
|
||||
}
|
||||
|
||||
data class SignupCredentials(val uri: URI?, val userName: String, val email: String, val password: String)
|
@ -0,0 +1,157 @@
|
||||
package com.etesync.syncadapter.ui.etebase
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.commit
|
||||
import com.etebase.client.CollectionAccessLevel
|
||||
import com.etesync.syncadapter.CachedCollection
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.resource.LocalCalendar
|
||||
import com.etesync.syncadapter.ui.BaseActivity
|
||||
import com.etesync.syncadapter.ui.WebViewActivity
|
||||
import com.etesync.syncadapter.utils.HintManager
|
||||
import com.etesync.syncadapter.utils.ShowcaseBuilder
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import tourguide.tourguide.ToolTip
|
||||
import java.util.*
|
||||
|
||||
class ViewCollectionFragment : Fragment() {
|
||||
private val collectionModel: CollectionViewModel by activityViewModels()
|
||||
private val itemsModel: ItemsViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val ret = inflater.inflate(R.layout.view_collection_fragment, container, false)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
collectionModel.observe(this) {
|
||||
(activity as? BaseActivity?)?.supportActionBar?.title = it.meta.name
|
||||
if (container != null) {
|
||||
initUi(inflater, ret, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
private fun initUi(inflater: LayoutInflater, container: View, cachedCollection: CachedCollection) {
|
||||
val title = container.findViewById<TextView>(R.id.display_name)
|
||||
if (!HintManager.getHintSeen(requireContext(), HINT_IMPORT)) {
|
||||
val tourGuide = ShowcaseBuilder.getBuilder(requireActivity())
|
||||
.setToolTip(ToolTip().setTitle(getString(R.string.tourguide_title)).setDescription(getString(R.string.account_showcase_import)).setGravity(Gravity.BOTTOM))
|
||||
.setPointer(null)
|
||||
tourGuide.mOverlay.setHoleRadius(0)
|
||||
tourGuide.playOn(title)
|
||||
HintManager.setHintSeen(requireContext(), HINT_IMPORT, true)
|
||||
}
|
||||
|
||||
val fab = container.findViewById<FloatingActionButton>(R.id.fab)
|
||||
fab?.setOnClickListener {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setIcon(R.drawable.ic_info_dark)
|
||||
.setTitle(R.string.use_native_apps_title)
|
||||
.setMessage(R.string.use_native_apps_body)
|
||||
.setNegativeButton(R.string.navigation_drawer_guide, { _: DialogInterface, _: Int -> WebViewActivity.openUrl(requireContext(), Constants.helpUri) })
|
||||
.setPositiveButton(android.R.string.yes) { _, _ -> }.show()
|
||||
}
|
||||
|
||||
val col = cachedCollection.col
|
||||
val meta = cachedCollection.meta
|
||||
val isAdmin = col.accessLevel == CollectionAccessLevel.Admin
|
||||
|
||||
val colorSquare = container.findViewById<View>(R.id.color)
|
||||
val color = LocalCalendar.parseColor(meta.color)
|
||||
when (meta.collectionType) {
|
||||
Constants.ETEBASE_TYPE_CALENDAR -> {
|
||||
colorSquare.setBackgroundColor(color)
|
||||
}
|
||||
Constants.ETEBASE_TYPE_TASKS -> {
|
||||
colorSquare.setBackgroundColor(color)
|
||||
val tasksNotShowing = container.findViewById<View>(R.id.tasks_not_showing)
|
||||
tasksNotShowing.visibility = View.VISIBLE
|
||||
}
|
||||
Constants.ETEBASE_TYPE_ADDRESS_BOOK -> {
|
||||
colorSquare.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
title.text = meta.name
|
||||
|
||||
val desc = container.findViewById<TextView>(R.id.description)
|
||||
desc.text = meta.description
|
||||
|
||||
val owner = container.findViewById<TextView>(R.id.owner)
|
||||
if (isAdmin) {
|
||||
owner.visibility = View.GONE
|
||||
} else {
|
||||
owner.visibility = View.VISIBLE
|
||||
owner.text = "Shared with us" // FIXME: Figure out how to represent it and don't use a hardcoded string
|
||||
}
|
||||
|
||||
itemsModel.observe(this) {
|
||||
val stats = container.findViewById<TextView>(R.id.stats)
|
||||
container.findViewById<View>(R.id.progressBar).visibility = View.GONE
|
||||
stats.text = String.format(Locale.getDefault(), "Change log items: %d", it.size)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.fragment_view_collection, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val cachedCollection = collectionModel.value!!
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.on_edit -> {
|
||||
if (cachedCollection.col.accessLevel == CollectionAccessLevel.Admin) {
|
||||
parentFragmentManager.commit {
|
||||
replace(R.id.fragment_container, EditCollectionFragment(cachedCollection))
|
||||
addToBackStack(EditCollectionFragment::class.java.name)
|
||||
}
|
||||
} else {
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
.setIcon(R.drawable.ic_info_dark)
|
||||
.setTitle(R.string.not_allowed_title)
|
||||
.setMessage(R.string.edit_owner_only_anon)
|
||||
.setPositiveButton(android.R.string.yes) { _, _ -> }.create()
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
R.id.on_manage_members -> {
|
||||
parentFragmentManager.commit {
|
||||
replace(R.id.fragment_container, CollectionMembersFragment())
|
||||
addToBackStack(null)
|
||||
}
|
||||
}
|
||||
R.id.on_import -> {
|
||||
if (cachedCollection.col.accessLevel != CollectionAccessLevel.ReadOnly) {
|
||||
parentFragmentManager.commit {
|
||||
replace(R.id.fragment_container, ImportCollectionFragment())
|
||||
addToBackStack(null)
|
||||
}
|
||||
} else {
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
.setIcon(R.drawable.ic_info_dark)
|
||||
.setTitle(R.string.not_allowed_title)
|
||||
.setMessage(R.string.edit_owner_only_anon)
|
||||
.setPositiveButton(android.R.string.yes) { _, _ -> }.create()
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val HINT_IMPORT = "Import"
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
import com.etesync.syncadapter.ui.BaseActivity
|
||||
|
||||
class ImportActivity : BaseActivity(), SelectImportMethod, ResultFragment.OnImportCallback, DialogInterface {
|
||||
class ImportActivity : BaseActivity(), SelectImportMethod, DialogInterface {
|
||||
|
||||
private lateinit var account: Account
|
||||
protected lateinit var info: CollectionInfo
|
||||
@ -83,13 +83,6 @@ class ImportActivity : BaseActivity(), SelectImportMethod, ResultFragment.OnImpo
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
override fun onImportResult(importResult: ResultFragment.ImportResult) {
|
||||
val fragment = ResultFragment.newInstance(importResult)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(fragment, "importResult")
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
finish()
|
||||
}
|
||||
@ -108,9 +101,9 @@ class ImportActivity : BaseActivity(), SelectImportMethod, ResultFragment.OnImpo
|
||||
// This makes sure that the container activity has implemented
|
||||
// the callback interface. If not, it throws an exception
|
||||
try {
|
||||
mSelectImportMethod = activity as SelectImportMethod?
|
||||
mSelectImportMethod = activity as SelectImportMethod
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(activity!!.toString() + " must implement MyInterface ")
|
||||
throw ClassCastException(activity.toString() + " must implement MyInterface ")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -14,13 +14,13 @@ import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.commit
|
||||
import at.bitfire.ical4android.*
|
||||
import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS
|
||||
import at.bitfire.vcard4android.BatchOperation
|
||||
import at.bitfire.vcard4android.Contact
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
|
||||
import com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO
|
||||
import com.etesync.syncadapter.CachedCollection
|
||||
import com.etesync.syncadapter.Constants.*
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.log.Logger
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
@ -35,19 +35,14 @@ import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
|
||||
|
||||
class ImportFragment : DialogFragment() {
|
||||
class ImportFragment(private val account: Account, private val uid: String, private val enumType: CollectionInfo.Type) : DialogFragment() {
|
||||
|
||||
private lateinit var account: Account
|
||||
private lateinit var info: CollectionInfo
|
||||
private var inputStream: InputStream? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
isCancelable = false
|
||||
retainInstance = true
|
||||
|
||||
account = arguments!!.getParcelable(KEY_ACCOUNT)!!
|
||||
info = arguments!!.getSerializable(KEY_COLLECTION_INFO) as CollectionInfo
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
@ -57,7 +52,7 @@ class ImportFragment : DialogFragment() {
|
||||
} else {
|
||||
val data = ImportResult()
|
||||
data.e = Exception(getString(R.string.import_permission_required))
|
||||
(activity as ResultFragment.OnImportCallback).onImportResult(data)
|
||||
onImportResult(data)
|
||||
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
@ -118,7 +113,7 @@ class ImportFragment : DialogFragment() {
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.action = Intent.ACTION_GET_CONTENT
|
||||
|
||||
when (info.enumType) {
|
||||
when (enumType) {
|
||||
CollectionInfo.Type.CALENDAR -> intent.type = "text/calendar"
|
||||
CollectionInfo.Type.TASKS -> intent.type = "text/calendar"
|
||||
CollectionInfo.Type.ADDRESS_BOOK -> intent.type = "text/x-vcard"
|
||||
@ -132,7 +127,7 @@ class ImportFragment : DialogFragment() {
|
||||
val data = ImportResult()
|
||||
data.e = Exception("Failed to open file chooser.\nPlease install one.")
|
||||
|
||||
(activity as ResultFragment.OnImportCallback).onImportResult(data)
|
||||
onImportResult(data)
|
||||
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
@ -146,7 +141,7 @@ class ImportFragment : DialogFragment() {
|
||||
if (data != null) {
|
||||
// Get the URI of the selected file
|
||||
val uri = data.data!!
|
||||
Logger.log.info("Starting import into ${info.uid} from file ${uri}")
|
||||
Logger.log.info("Starting import into ${uid} from file ${uri}")
|
||||
try {
|
||||
inputStream = activity!!.contentResolver.openInputStream(uri)
|
||||
|
||||
@ -157,7 +152,7 @@ class ImportFragment : DialogFragment() {
|
||||
val importResult = ImportResult()
|
||||
importResult.e = e
|
||||
|
||||
(activity as ResultFragment.OnImportCallback).onImportResult(importResult)
|
||||
onImportResult(importResult)
|
||||
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
@ -172,7 +167,7 @@ class ImportFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
fun loadFinished(data: ImportResult) {
|
||||
(activity as ResultFragment.OnImportCallback).onImportResult(data)
|
||||
onImportResult(data)
|
||||
|
||||
Logger.log.info("Finished import")
|
||||
|
||||
@ -217,7 +212,7 @@ class ImportFragment : DialogFragment() {
|
||||
val context = context!!
|
||||
val importReader = InputStreamReader(inputStream)
|
||||
|
||||
if (info.enumType == CollectionInfo.Type.CALENDAR) {
|
||||
if (enumType == CollectionInfo.Type.CALENDAR) {
|
||||
val events = Event.eventsFromReader(importReader, null)
|
||||
importReader.close()
|
||||
|
||||
@ -239,7 +234,7 @@ class ImportFragment : DialogFragment() {
|
||||
|
||||
val localCalendar: LocalCalendar?
|
||||
try {
|
||||
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, info.uid!!)
|
||||
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, uid!!)
|
||||
if (localCalendar == null) {
|
||||
throw FileNotFoundException("Failed to load local resource.")
|
||||
}
|
||||
@ -270,7 +265,7 @@ class ImportFragment : DialogFragment() {
|
||||
|
||||
entryProcessed()
|
||||
}
|
||||
} else if (info.enumType == CollectionInfo.Type.TASKS) {
|
||||
} else if (enumType == CollectionInfo.Type.TASKS) {
|
||||
val tasks = Task.tasksFromReader(importReader)
|
||||
importReader.close()
|
||||
|
||||
@ -297,7 +292,7 @@ class ImportFragment : DialogFragment() {
|
||||
provider?.let {
|
||||
val localTaskList: LocalTaskList?
|
||||
try {
|
||||
localTaskList = LocalTaskList.findByName(account, it, LocalTaskList.Factory, info.uid!!)
|
||||
localTaskList = LocalTaskList.findByName(account, it, LocalTaskList.Factory, uid!!)
|
||||
if (localTaskList == null) {
|
||||
throw FileNotFoundException("Failed to load local resource.")
|
||||
}
|
||||
@ -325,7 +320,7 @@ class ImportFragment : DialogFragment() {
|
||||
entryProcessed()
|
||||
}
|
||||
}
|
||||
} else if (info.enumType == CollectionInfo.Type.ADDRESS_BOOK) {
|
||||
} else if (enumType == CollectionInfo.Type.ADDRESS_BOOK) {
|
||||
val uidToLocalId = HashMap<String?, Long>()
|
||||
val downloader = ContactsSyncManager.ResourceDownloader(context)
|
||||
val contacts = Contact.fromReader(importReader, downloader)
|
||||
@ -346,7 +341,7 @@ class ImportFragment : DialogFragment() {
|
||||
return result
|
||||
}
|
||||
|
||||
val localAddressBook = LocalAddressBook.findByUid(context, provider, account, info.uid!!)
|
||||
val localAddressBook = LocalAddressBook.findByUid(context, provider, account, uid!!)
|
||||
if (localAddressBook == null) {
|
||||
throw FileNotFoundException("Failed to load local address book.")
|
||||
}
|
||||
@ -424,18 +419,30 @@ class ImportFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onImportResult(importResult: ImportResult) {
|
||||
val fragment = ResultFragment.newInstance(importResult)
|
||||
parentFragmentManager.commit(true) {
|
||||
add(fragment, "importResult")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val REQUEST_CODE = 6384 // onActivityResult request
|
||||
|
||||
private val TAG_PROGRESS_MAX = "progressMax"
|
||||
|
||||
fun newInstance(account: Account, info: CollectionInfo): ImportFragment {
|
||||
val frag = ImportFragment()
|
||||
val args = Bundle(1)
|
||||
args.putParcelable(KEY_ACCOUNT, account)
|
||||
args.putSerializable(KEY_COLLECTION_INFO, info)
|
||||
frag.arguments = args
|
||||
return frag
|
||||
return ImportFragment(account, info.uid!!, info.enumType!!)
|
||||
}
|
||||
|
||||
fun newInstance(account: Account, cachedCollection: CachedCollection): ImportFragment {
|
||||
val enumType = when (cachedCollection.meta.collectionType) {
|
||||
ETEBASE_TYPE_CALENDAR -> CollectionInfo.Type.CALENDAR
|
||||
ETEBASE_TYPE_TASKS -> CollectionInfo.Type.TASKS
|
||||
ETEBASE_TYPE_ADDRESS_BOOK -> CollectionInfo.Type.ADDRESS_BOOK
|
||||
else -> throw Exception("Got unsupported collection type")
|
||||
}
|
||||
return ImportFragment(account, cachedCollection.col.uid, enumType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,8 @@ import android.widget.ExpandableListView
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.ListFragment
|
||||
import androidx.fragment.app.commit
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
|
||||
import com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.log.Logger
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
@ -24,17 +23,10 @@ import com.etesync.syncadapter.resource.LocalCalendar
|
||||
import com.etesync.syncadapter.resource.LocalEvent
|
||||
|
||||
|
||||
class LocalCalendarImportFragment : ListFragment() {
|
||||
|
||||
private lateinit var account: Account
|
||||
private lateinit var info: CollectionInfo
|
||||
|
||||
class LocalCalendarImportFragment(private val account: Account, private val uid: String) : ListFragment() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
retainInstance = true
|
||||
|
||||
account = arguments!!.getParcelable(KEY_ACCOUNT)!!
|
||||
info = arguments!!.getSerializable(KEY_COLLECTION_INFO) as CollectionInfo
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
@ -200,7 +192,7 @@ class LocalCalendarImportFragment : ListFragment() {
|
||||
if (progressDialog.isShowing && !activity.isDestroyed) {
|
||||
progressDialog.dismiss()
|
||||
}
|
||||
(activity as ResultFragment.OnImportCallback).onImportResult(result)
|
||||
onImportResult(result)
|
||||
}
|
||||
|
||||
private fun importEvents(fromCalendar: LocalCalendar): ResultFragment.ImportResult {
|
||||
@ -208,7 +200,7 @@ class LocalCalendarImportFragment : ListFragment() {
|
||||
try {
|
||||
val localCalendar = LocalCalendar.findByName(account,
|
||||
context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!,
|
||||
LocalCalendar.Factory, info!!.uid!!)
|
||||
LocalCalendar.Factory, uid)
|
||||
val localEvents = fromCalendar.findAll()
|
||||
val total = localEvents.size
|
||||
progressDialog.max = total
|
||||
@ -248,15 +240,17 @@ class LocalCalendarImportFragment : ListFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onImportResult(importResult: ResultFragment.ImportResult) {
|
||||
val fragment = ResultFragment.newInstance(importResult)
|
||||
parentFragmentManager.commit(true) {
|
||||
add(fragment, "importResult")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(account: Account, info: CollectionInfo): LocalCalendarImportFragment {
|
||||
val frag = LocalCalendarImportFragment()
|
||||
val args = Bundle(1)
|
||||
args.putParcelable(KEY_ACCOUNT, account)
|
||||
args.putSerializable(KEY_COLLECTION_INFO, info)
|
||||
frag.arguments = args
|
||||
return frag
|
||||
return LocalCalendarImportFragment(account, info.uid!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,11 +18,10 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
|
||||
import com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.log.Logger
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
@ -32,18 +31,12 @@ import com.etesync.syncadapter.resource.LocalGroup
|
||||
import java.util.*
|
||||
|
||||
|
||||
class LocalContactImportFragment : Fragment() {
|
||||
|
||||
private lateinit var account: Account
|
||||
private lateinit var info: CollectionInfo
|
||||
class LocalContactImportFragment(private val account: Account, private val uid: String) : Fragment() {
|
||||
private var recyclerView: RecyclerView? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
retainInstance = true
|
||||
|
||||
account = arguments!!.getParcelable(KEY_ACCOUNT)
|
||||
info = arguments!!.getSerializable(KEY_COLLECTION_INFO) as CollectionInfo
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
@ -134,15 +127,16 @@ class LocalContactImportFragment : Fragment() {
|
||||
if (progressDialog.isShowing && !activity.isDestroyed) {
|
||||
progressDialog.dismiss()
|
||||
}
|
||||
(activity as ResultFragment.OnImportCallback).onImportResult(result)
|
||||
onImportResult(result)
|
||||
}
|
||||
|
||||
private fun importContacts(localAddressBook: LocalAddressBook): ResultFragment.ImportResult {
|
||||
val result = ResultFragment.ImportResult()
|
||||
try {
|
||||
val addressBook = LocalAddressBook.findByUid(context!!,
|
||||
context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!,
|
||||
account, info.uid!!)!!
|
||||
val addressBook = LocalAddressBook.findByUid(requireContext(),
|
||||
requireContext().contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!,
|
||||
account, uid)
|
||||
?: throw Exception("Could not find address book")
|
||||
val localContacts = localAddressBook.findAllContacts()
|
||||
val localGroups = localAddressBook.findAllGroups()
|
||||
val oldIdToNewId = HashMap<Long, Long>()
|
||||
@ -214,6 +208,13 @@ class LocalContactImportFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onImportResult(importResult: ResultFragment.ImportResult) {
|
||||
val fragment = ResultFragment.newInstance(importResult)
|
||||
parentFragmentManager.commit(true) {
|
||||
add(fragment, "importResult")
|
||||
}
|
||||
}
|
||||
|
||||
class ImportContactAdapter
|
||||
/**
|
||||
* Initialize the dataset of the Adapter.
|
||||
@ -316,13 +317,7 @@ class LocalContactImportFragment : Fragment() {
|
||||
companion object {
|
||||
|
||||
fun newInstance(account: Account, info: CollectionInfo): LocalContactImportFragment {
|
||||
val frag = LocalContactImportFragment()
|
||||
val args = Bundle(1)
|
||||
args.putParcelable(KEY_ACCOUNT, account)
|
||||
args.putSerializable(KEY_COLLECTION_INFO, info)
|
||||
frag.arguments = args
|
||||
|
||||
return frag
|
||||
return LocalContactImportFragment(account, info.uid!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ class ResultFragment : DialogFragment() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
result = arguments!!.getSerializable(KEY_RESULT) as ImportResult
|
||||
result = requireArguments().getSerializable(KEY_RESULT) as ImportResult
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
@ -32,7 +32,7 @@ class ResultFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
if (result!!.isFailed) {
|
||||
return AlertDialog.Builder(activity!!)
|
||||
return AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.import_dialog_failed_title)
|
||||
.setIcon(R.drawable.ic_error_dark)
|
||||
.setMessage(getString(R.string.import_dialog_failed_body, result!!.e!!.localizedMessage))
|
||||
@ -72,10 +72,6 @@ class ResultFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
interface OnImportCallback {
|
||||
fun onImportResult(importResult: ImportResult)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val KEY_RESULT = "result"
|
||||
|
||||
|
@ -88,7 +88,7 @@ class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener {
|
||||
|
||||
override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
|
||||
val entry = listAdapter?.getItem(position) as EntryEntity
|
||||
startActivity(JournalItemActivity.newIntent(context!!, account, info, entry.content))
|
||||
startActivity(JournalItemActivity.newIntent(requireContext(), account, info, entry.content))
|
||||
}
|
||||
|
||||
internal inner class EntriesListAdapter(context: Context) : ArrayAdapter<EntryEntity>(context, R.layout.journal_viewer_list_item) {
|
||||
|
@ -8,16 +8,21 @@
|
||||
package com.etesync.syncadapter.ui.setup
|
||||
|
||||
import android.content.Context
|
||||
import com.etebase.client.Account
|
||||
import com.etebase.client.Client
|
||||
import com.etebase.client.exceptions.EtebaseException
|
||||
import com.etesync.syncadapter.HttpClient
|
||||
import com.etesync.journalmanager.Crypto
|
||||
import com.etesync.journalmanager.Exceptions
|
||||
import com.etesync.journalmanager.JournalAuthenticator
|
||||
import com.etesync.journalmanager.UserInfoManager
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.log.Logger
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.IOException
|
||||
import java.io.Serializable
|
||||
import java.net.URI
|
||||
@ -30,45 +35,102 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred
|
||||
httpClient = HttpClient.Builder(context).build().okHttpClient
|
||||
}
|
||||
|
||||
private fun isServerEtebase(): Boolean {
|
||||
if (credentials.uri != null) {
|
||||
val remote = credentials.uri.toHttpUrlOrNull()!!.newBuilder()
|
||||
.addPathSegments("api/v1/authentication/is_etebase/")
|
||||
.build()
|
||||
|
||||
fun findInitialConfiguration(): Configuration {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url(remote)
|
||||
.build()
|
||||
|
||||
val response = httpClient.newCall(request).execute()
|
||||
|
||||
return response.isSuccessful
|
||||
} else {
|
||||
return !credentials.userName.contains("@")
|
||||
}
|
||||
}
|
||||
|
||||
fun findInitialConfigurationLegacy(): Configuration {
|
||||
var exception: Throwable? = null
|
||||
val cardDavConfig = findInitialConfiguration(CollectionInfo.Type.ADDRESS_BOOK)
|
||||
val calDavConfig = findInitialConfiguration(CollectionInfo.Type.CALENDAR)
|
||||
|
||||
val authenticator = JournalAuthenticator(httpClient, credentials.uri?.toHttpUrlOrNull()!!)
|
||||
val uri = credentials.uri ?: URI(Constants.serviceUrl.toString())
|
||||
|
||||
val authenticator = JournalAuthenticator(httpClient, uri.toHttpUrlOrNull()!!)
|
||||
|
||||
var authtoken: String? = null
|
||||
var userInfo: UserInfoManager.UserInfo? = null
|
||||
try {
|
||||
authtoken = authenticator.getAuthToken(credentials.userName, credentials.password)
|
||||
|
||||
val authenticatedHttpClient = HttpClient.Builder(context, credentials.uri.host, authtoken!!).build().okHttpClient
|
||||
val userInfoManager = UserInfoManager(authenticatedHttpClient, credentials.uri.toHttpUrlOrNull()!!)
|
||||
val authenticatedHttpClient = HttpClient.Builder(context, uri.host, authtoken!!).build().okHttpClient
|
||||
val userInfoManager = UserInfoManager(authenticatedHttpClient, uri.toHttpUrlOrNull()!!)
|
||||
userInfo = userInfoManager.fetch(credentials.userName)
|
||||
} catch (e: Exceptions.HttpException) {
|
||||
Logger.log.warning(e.message)
|
||||
Logger.log.warning(e.localizedMessage)
|
||||
exception = e
|
||||
} catch (e: IOException) {
|
||||
Logger.log.warning(e.message)
|
||||
Logger.log.warning(e.localizedMessage)
|
||||
exception = e
|
||||
}
|
||||
|
||||
return Configuration(
|
||||
credentials.uri,
|
||||
credentials.userName, authtoken,
|
||||
cardDavConfig, calDavConfig,
|
||||
uri,
|
||||
credentials.userName,
|
||||
null,
|
||||
authtoken,
|
||||
userInfo,
|
||||
exception
|
||||
)
|
||||
}
|
||||
|
||||
protected fun findInitialConfiguration(service: CollectionInfo.Type): Configuration.ServiceInfo {
|
||||
// put discovered information here
|
||||
val config = Configuration.ServiceInfo()
|
||||
Logger.log.info("Finding initial " + service.toString() + " service configuration")
|
||||
fun findInitialConfigurationEtebase(): Configuration {
|
||||
var exception: Throwable? = null
|
||||
|
||||
return config
|
||||
val uri = credentials.uri ?: URI(Constants.etebaseServiceUrl)
|
||||
|
||||
var etebaseSession: String? = null
|
||||
try {
|
||||
val client = Client.create(httpClient, uri.toString())
|
||||
val etebase = Account.login(client, credentials.userName, credentials.password)
|
||||
etebaseSession = etebase.save(null)
|
||||
} catch (e: java.lang.Exception) {
|
||||
Logger.log.warning(e.localizedMessage)
|
||||
exception = e
|
||||
}
|
||||
|
||||
return Configuration(
|
||||
uri,
|
||||
credentials.userName,
|
||||
etebaseSession,
|
||||
null,
|
||||
null,
|
||||
exception
|
||||
)
|
||||
}
|
||||
|
||||
fun findInitialConfiguration(): Configuration {
|
||||
try {
|
||||
if (isServerEtebase()) {
|
||||
Logger.log.fine("Attempting to login to etebase")
|
||||
return findInitialConfigurationEtebase()
|
||||
} else {
|
||||
Logger.log.fine("Attempting to login to EteSync legacy")
|
||||
return findInitialConfigurationLegacy()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return Configuration(
|
||||
credentials.uri,
|
||||
credentials.userName,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// data classes
|
||||
@ -76,7 +138,7 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred
|
||||
class Configuration
|
||||
// We have to use URI here because HttpUrl is not serializable!
|
||||
|
||||
(val url: URI, val userName: String, val authtoken: String?, val cardDAV: ServiceInfo, val calDAV: ServiceInfo, var userInfo: UserInfoManager.UserInfo?, var error: Throwable?) : Serializable {
|
||||
(val url: URI?, val userName: String, val etebaseSession: String?, val authtoken: String?, var userInfo: UserInfoManager.UserInfo?, var error: Throwable?) : Serializable {
|
||||
var rawPassword: String? = null
|
||||
var password: String? = null
|
||||
var keyPair: Crypto.AsymmetricKeyPair? = null
|
||||
@ -84,6 +146,9 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred
|
||||
val isFailed: Boolean
|
||||
get() = this.error != null
|
||||
|
||||
val isLegacy: Boolean
|
||||
get() = this.authtoken != null
|
||||
|
||||
class ServiceInfo : Serializable {
|
||||
val collections: Map<String, CollectionInfo> = HashMap()
|
||||
|
||||
@ -93,7 +158,7 @@ class BaseConfigurationFinder(protected val context: Context, protected val cred
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "BaseConfigurationFinder.Configuration(url=" + this.url + ", userName=" + this.userName + ", keyPair=" + this.keyPair + ", cardDAV=" + this.cardDAV + ", calDAV=" + this.calDAV + ", error=" + this.error + ", failed=" + this.isFailed + ")"
|
||||
return "BaseConfigurationFinder.Configuration(url=" + this.url + ", userName=" + this.userName + ", keyPair=" + this.keyPair + ", error=" + this.error + ", failed=" + this.isFailed + ")"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright © 2013 – 2016 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 com.etesync.syncadapter.ui.setup
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.app.ProgressDialog
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS
|
||||
import com.etesync.syncadapter.*
|
||||
import com.etesync.syncadapter.log.Logger
|
||||
import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration
|
||||
import com.etesync.syncadapter.utils.AndroidCompat
|
||||
import com.etesync.syncadapter.utils.TaskProviderHandling
|
||||
import java.util.logging.Level
|
||||
|
||||
class CreateAccountFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val progress = ProgressDialog(activity)
|
||||
progress.setTitle(R.string.setting_up_encryption)
|
||||
progress.setMessage(getString(R.string.setting_up_encryption_content))
|
||||
progress.isIndeterminate = true
|
||||
progress.setCanceledOnTouchOutside(false)
|
||||
isCancelable = false
|
||||
return progress
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val config = requireArguments().getSerializable(KEY_CONFIG) as Configuration
|
||||
|
||||
val activity = requireActivity()
|
||||
if (createAccount(config.userName, config)) {
|
||||
activity.setResult(Activity.RESULT_OK)
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(InvalidAccountException::class)
|
||||
protected fun createAccount(accountName: String, config: Configuration): Boolean {
|
||||
val account = Account(accountName, App.accountType)
|
||||
|
||||
// create Android account
|
||||
Logger.log.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, config.userName, config.url))
|
||||
|
||||
val accountManager = AccountManager.get(context)
|
||||
if (!accountManager.addAccountExplicitly(account, config.password, null))
|
||||
return false
|
||||
|
||||
AccountSettings.setUserData(accountManager, account, config.url, config.userName)
|
||||
|
||||
// add entries for account to service DB
|
||||
Logger.log.log(Level.INFO, "Writing account configuration to database", config)
|
||||
try {
|
||||
val settings = AccountSettings(requireContext(), account)
|
||||
|
||||
settings.etebaseSession = config.etebaseSession
|
||||
|
||||
// contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
|
||||
settings.setSyncInterval(App.addressBooksAuthority, Constants.DEFAULT_SYNC_INTERVAL.toLong())
|
||||
|
||||
// calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
|
||||
settings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL.toLong())
|
||||
|
||||
OPENTASK_PROVIDERS.forEach {
|
||||
// enable task sync if OpenTasks is installed
|
||||
// further changes will be handled by PackageChangedReceiver
|
||||
TaskProviderHandling.updateTaskSync(requireContext(), it)
|
||||
}
|
||||
|
||||
} catch (e: InvalidAccountException) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't access account settings", e)
|
||||
AndroidCompat.removeAccount(accountManager, account)
|
||||
throw e
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val KEY_CONFIG = "config"
|
||||
|
||||
fun newInstance(config: Configuration): CreateAccountFragment {
|
||||
val frag = CreateAccountFragment()
|
||||
val args = Bundle(1)
|
||||
args.putSerializable(KEY_CONFIG, config)
|
||||
frag.arguments = args
|
||||
return frag
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.loader.app.LoaderManager
|
||||
@ -27,8 +28,8 @@ class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallba
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val progress = ProgressDialog(activity)
|
||||
progress.setTitle(R.string.login_configuration_detection)
|
||||
progress.setMessage(getString(R.string.login_querying_server))
|
||||
progress.setTitle(R.string.setting_up_encryption)
|
||||
progress.setMessage(getString(R.string.setting_up_encryption_content))
|
||||
progress.isIndeterminate = true
|
||||
progress.setCanceledOnTouchOutside(false)
|
||||
isCancelable = false
|
||||
@ -38,26 +39,36 @@ class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallba
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Logger.log.fine("DetectConfigurationFragment: loading")
|
||||
loaderManager.initLoader(0, arguments, this)
|
||||
}
|
||||
|
||||
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Configuration> {
|
||||
return ServerConfigurationLoader(context!!, args!!.getParcelable(ARG_LOGIN_CREDENTIALS) as LoginCredentials)
|
||||
return ServerConfigurationLoader(requireContext(), args!!.getParcelable(ARG_LOGIN_CREDENTIALS) as LoginCredentials)
|
||||
}
|
||||
|
||||
override fun onLoadFinished(loader: Loader<Configuration>, data: Configuration?) {
|
||||
if (data != null) {
|
||||
if (data.isFailed)
|
||||
// no service found: show error message
|
||||
fragmentManager!!.beginTransaction()
|
||||
if (data.isFailed) {
|
||||
Logger.log.warning("Failed login configuration ${data.error?.localizedMessage}")
|
||||
// no service found: show error message
|
||||
requireFragmentManager().beginTransaction()
|
||||
.add(NothingDetectedFragment.newInstance(data.error!!.localizedMessage), null)
|
||||
.commitAllowingStateLoss()
|
||||
else
|
||||
// service found: continue
|
||||
fragmentManager!!.beginTransaction()
|
||||
} else if (data.isLegacy) {
|
||||
// legacy service found: continue
|
||||
Logger.log.info("Found legacy account - asking for encryption details")
|
||||
requireFragmentManager().beginTransaction()
|
||||
.replace(android.R.id.content, EncryptionDetailsFragment.newInstance(data))
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss()
|
||||
} else {
|
||||
Logger.log.info("Found Etebase account account")
|
||||
requireFragmentManager().beginTransaction()
|
||||
.replace(android.R.id.content, CreateAccountFragment.newInstance(data))
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
} else
|
||||
Logger.log.severe("Configuration detection failed")
|
||||
|
||||
@ -71,14 +82,9 @@ class DetectConfigurationFragment : DialogFragment(), LoaderManager.LoaderCallba
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return AlertDialog.Builder(activity!!)
|
||||
.setTitle(R.string.login_configuration_detection)
|
||||
.setTitle(R.string.setting_up_encryption)
|
||||
.setIcon(R.drawable.ic_error_dark)
|
||||
.setMessage(R.string.login_wrong_username_or_password)
|
||||
.setNeutralButton(R.string.login_view_logs) { dialog, which ->
|
||||
val intent = DebugInfoActivity.newIntent(context, this::class.toString())
|
||||
intent.putExtra(DebugInfoActivity.KEY_LOGS, arguments!!.getString(KEY_LOGS))
|
||||
startActivity(intent)
|
||||
}
|
||||
.setMessage(requireArguments().getString(KEY_LOGS))
|
||||
.setPositiveButton(android.R.string.ok) { dialog, which ->
|
||||
// dismiss
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ class LoginActivity : BaseActivity() {
|
||||
if (savedInstanceState == null)
|
||||
// first call, add fragment
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(android.R.id.content, LoginCredentialsFragment())
|
||||
.replace(android.R.id.content, LoginCredentialsFragment(null, null))
|
||||
.commit()
|
||||
|
||||
}
|
||||
@ -43,16 +43,4 @@ class LoginActivity : BaseActivity() {
|
||||
fun showHelp(item: MenuItem) {
|
||||
WebViewActivity.openUrl(this, Constants.helpUri)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* When set, and [.EXTRA_PASSWORD] is set too, the user name field will be set to this value.
|
||||
*/
|
||||
val EXTRA_USERNAME = "username"
|
||||
|
||||
/**
|
||||
* When set, the password field will be set to this value.
|
||||
*/
|
||||
val EXTRA_PASSWORD = "password"
|
||||
}
|
||||
}
|
||||
|
@ -15,22 +15,7 @@ import com.etesync.syncadapter.log.Logger
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
|
||||
class LoginCredentials(_uri: URI?, val userName: String, val password: String) : Parcelable {
|
||||
val uri: URI?
|
||||
|
||||
init {
|
||||
var uri = _uri
|
||||
|
||||
if (uri == null) {
|
||||
try {
|
||||
uri = URI(Constants.serviceUrl.toString())
|
||||
} catch (e: URISyntaxException) {
|
||||
Logger.log.severe("Should never happen, it's a constant")
|
||||
}
|
||||
|
||||
}
|
||||
this.uri = uri
|
||||
}
|
||||
class LoginCredentials(val uri: URI?, val userName: String, val password: String) : Parcelable {
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
|
@ -12,7 +12,6 @@ import android.accounts.Account
|
||||
import android.app.Dialog
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
@ -32,8 +31,8 @@ class LoginCredentialsChangeFragment : DialogFragment(), LoaderManager.LoaderCal
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val progress = ProgressDialog(activity)
|
||||
progress.setTitle(R.string.login_configuration_detection)
|
||||
progress.setMessage(getString(R.string.login_querying_server))
|
||||
progress.setTitle(R.string.setting_up_encryption)
|
||||
progress.setMessage(getString(R.string.setting_up_encryption_content))
|
||||
progress.isIndeterminate = true
|
||||
progress.setCanceledOnTouchOutside(false)
|
||||
isCancelable = false
|
||||
@ -84,7 +83,7 @@ class LoginCredentialsChangeFragment : DialogFragment(), LoaderManager.LoaderCal
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return AlertDialog.Builder(activity!!)
|
||||
.setTitle(R.string.login_configuration_detection)
|
||||
.setTitle(R.string.setting_up_encryption)
|
||||
.setIcon(R.drawable.ic_error_dark)
|
||||
.setMessage(R.string.login_wrong_username_or_password)
|
||||
.setNeutralButton(R.string.login_view_logs) { dialog, which ->
|
||||
|
@ -17,15 +17,19 @@ import android.widget.CheckedTextView
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.replace
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
import com.etesync.syncadapter.ui.WebViewActivity
|
||||
import com.etesync.syncadapter.ui.etebase.SignupFragment
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import net.cachapa.expandablelayout.ExpandableLayout
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import java.net.URI
|
||||
|
||||
class LoginCredentialsFragment : Fragment() {
|
||||
class LoginCredentialsFragment(private val initialUsername: String?, private val initialPassword: String?) : Fragment() {
|
||||
internal lateinit var editUserName: EditText
|
||||
internal lateinit var editUrlPassword: TextInputLayout
|
||||
|
||||
@ -36,28 +40,21 @@ class LoginCredentialsFragment : Fragment() {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val v = inflater.inflate(R.layout.login_credentials_fragment, container, false)
|
||||
|
||||
editUserName = v.findViewById<View>(R.id.user_name) as EditText
|
||||
editUrlPassword = v.findViewById<View>(R.id.url_password) as TextInputLayout
|
||||
showAdvanced = v.findViewById<View>(R.id.show_advanced) as CheckedTextView
|
||||
customServer = v.findViewById<View>(R.id.custom_server) as EditText
|
||||
editUserName = v.findViewById<TextInputEditText>(R.id.user_name)
|
||||
editUrlPassword = v.findViewById<TextInputLayout>(R.id.url_password)
|
||||
showAdvanced = v.findViewById<CheckedTextView>(R.id.show_advanced)
|
||||
customServer = v.findViewById<TextInputEditText>(R.id.custom_server)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val activity = activity
|
||||
val intent = activity?.intent
|
||||
if (intent != null) {
|
||||
// we've got initial login data
|
||||
val username = intent.getStringExtra(LoginActivity.EXTRA_USERNAME)
|
||||
val password = intent.getStringExtra(LoginActivity.EXTRA_PASSWORD)
|
||||
|
||||
editUserName.setText(username)
|
||||
editUrlPassword.editText?.setText(password)
|
||||
}
|
||||
editUserName.setText(initialUsername ?: "")
|
||||
editUrlPassword.editText?.setText(initialPassword ?: "")
|
||||
}
|
||||
|
||||
val createAccount = v.findViewById<View>(R.id.create_account) as Button
|
||||
createAccount.setOnClickListener {
|
||||
val createUri = Constants.registrationUrl.buildUpon().appendQueryParameter("email", editUserName.text.toString()).build()
|
||||
WebViewActivity.openUrl(context!!, createUri)
|
||||
parentFragmentManager.commit {
|
||||
replace(android.R.id.content, SignupFragment(editUserName.text.toString(), editUrlPassword.editText?.text.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
val login = v.findViewById<View>(R.id.login) as Button
|
||||
@ -92,12 +89,16 @@ class LoginCredentialsFragment : Fragment() {
|
||||
if (userName.isEmpty()) {
|
||||
editUserName.error = getString(R.string.login_email_address_error)
|
||||
valid = false
|
||||
} else {
|
||||
editUserName.error = null
|
||||
}
|
||||
|
||||
val password = editUrlPassword.editText?.text.toString()
|
||||
if (password.isEmpty()) {
|
||||
editUrlPassword.error = getString(R.string.login_password_required)
|
||||
valid = false
|
||||
} else {
|
||||
editUrlPassword.error = null
|
||||
}
|
||||
|
||||
var uri: URI? = null
|
||||
@ -108,6 +109,7 @@ class LoginCredentialsFragment : Fragment() {
|
||||
val url = server.toHttpUrlOrNull()
|
||||
if (url != null) {
|
||||
uri = url.toUri()
|
||||
customServer.error = null
|
||||
} else {
|
||||
customServer.error = getString(R.string.login_custom_server_error)
|
||||
valid = false
|
||||
|
@ -134,13 +134,13 @@ class SetupEncryptionFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
// insert CardDAV service
|
||||
insertService(accountName, CollectionInfo.Type.ADDRESS_BOOK, config.cardDAV)
|
||||
insertService(accountName, CollectionInfo.Type.ADDRESS_BOOK)
|
||||
|
||||
// contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
|
||||
settings.setSyncInterval(App.addressBooksAuthority, Constants.DEFAULT_SYNC_INTERVAL.toLong())
|
||||
|
||||
// insert CalDAV service
|
||||
insertService(accountName, CollectionInfo.Type.CALENDAR, config.calDAV)
|
||||
insertService(accountName, CollectionInfo.Type.CALENDAR)
|
||||
|
||||
// calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
|
||||
settings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL.toLong())
|
||||
@ -160,8 +160,9 @@ class SetupEncryptionFragment : DialogFragment() {
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun insertService(accountName: String, serviceType: CollectionInfo.Type, info: BaseConfigurationFinder.Configuration.ServiceInfo) {
|
||||
val data = (context!!.applicationContext as App).data
|
||||
protected fun insertService(accountName: String, serviceType: CollectionInfo.Type) {
|
||||
val info = Configuration.ServiceInfo()
|
||||
val data = (requireContext().applicationContext as App).data
|
||||
|
||||
// insert service
|
||||
val serviceEntity = ServiceEntity.fetchOrCreate(data, accountName, serviceType)
|
||||
|
@ -28,6 +28,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/please_wait"
|
||||
android:textIsSelectable="true"
|
||||
android:typeface="monospace" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -40,6 +40,7 @@
|
||||
android:id="@+id/encryption_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="14dp"
|
||||
app:passwordToggleEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
|
11
app/src/main/res/layout/etebase_collection_activity.xml
Normal file
11
app/src/main/res/layout/etebase_collection_activity.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
56
app/src/main/res/layout/etebase_view_collection_members.xml
Normal file
56
app/src/main/res/layout/etebase_view_collection_members.xml
Normal file
@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
layout="@layout/collection_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/activity_margin" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/activity_margin">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="right" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/add_member"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/activity_margin"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/collection_members_add"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_account_add_dark" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<fragment android:name="com.etesync.syncadapter.ui.etebase.CollectionMembersListFragment"
|
||||
android:id="@+id/list_entries_container"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
layout="@layout/collection_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/activity_margin" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/activity_margin">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="right" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:text="@string/collection_members_no_access" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/leave"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="14dp"
|
||||
android:text="@string/collection_members_leave" />
|
||||
|
||||
</LinearLayout>
|
29
app/src/main/res/layout/invitation_alert_dialog.xml
Normal file
29
app/src/main/res/layout/invitation_alert_dialog.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="18dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:text="@string/invitations_accept_reject_dialog"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fingerprint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:gravity="center"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
tools:text="aaaa 1111 bbbb cccc dddd eeee\n1111" />
|
||||
|
||||
</LinearLayout>
|
20
app/src/main/res/layout/invitations_list.xml
Normal file
20
app/src/main/res/layout/invitations_list.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ListView
|
||||
android:id="@id/android:list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@id/android:empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/invitations_loading"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
|
||||
</LinearLayout>
|
27
app/src/main/res/layout/invitations_list_item.xml
Normal file
27
app/src/main/res/layout/invitations_list_item.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
tools:text="Title" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/read_only"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:src="@drawable/ic_readonly_dark"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
@ -29,19 +29,19 @@
|
||||
android:text="@string/login_enter_service_details"
|
||||
android:layout_marginBottom="14dp"/>
|
||||
|
||||
<TextView
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_service_details_description"
|
||||
android:layout_marginBottom="14dp"/>
|
||||
android:layout_marginBottom="14dp">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/user_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_username"
|
||||
android:autofillHints="emailAddress"
|
||||
android:inputType="textEmailAddress"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/user_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_email_address"
|
||||
android:autofillHints="emailAddress"
|
||||
android:inputType="textEmailAddress"/>
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/url_password"
|
||||
android:layout_width="match_parent"
|
||||
@ -81,12 +81,16 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/custom_server"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_custom_server"
|
||||
android:inputType="textUri"/>
|
||||
android:layout_height="wrap_content">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/custom_server"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_custom_server"
|
||||
android:inputType="textUri"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</net.cachapa.expandablelayout.ExpandableLayout>
|
||||
|
||||
|
134
app/src/main/res/layout/signup_fragment.xml
Normal file
134
app/src/main/res/layout/signup_fragment.xml
Normal file
@ -0,0 +1,134 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright © 2013 – 2016 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
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ScrollView android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_margin="@dimen/activity_margin">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/login_type_headline"
|
||||
android:text="@string/signup_title"
|
||||
android:layout_marginBottom="14dp"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/user_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="14dp">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_username"
|
||||
android:autofillHints="emailAddress"
|
||||
android:inputType="textEmailAddress"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/email"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="14dp">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_email_address"
|
||||
android:autofillHints="emailAddress"
|
||||
android:inputType="textEmailAddress"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/url_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:passwordToggleEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:autofillHints="password"
|
||||
android:inputType="textPassword"
|
||||
android:hint="@string/login_password"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_encryption_check_password"/>
|
||||
|
||||
<CheckedTextView
|
||||
android:id="@+id/show_advanced"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:textSize="18sp"
|
||||
android:gravity="center_vertical"
|
||||
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
|
||||
android:text="@string/login_toggle_advanced" />
|
||||
|
||||
<net.cachapa.expandablelayout.ExpandableLayout
|
||||
android:id="@+id/advanced_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/custom_server"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_custom_server"
|
||||
android:inputType="textUri"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</net.cachapa.expandablelayout.ExpandableLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/stepper_nav_bar">
|
||||
|
||||
<Button
|
||||
android:id="@+id/login"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/login_login"
|
||||
style="@style/stepper_nav_button"/>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
style="@style/stepper_nav_button"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/create_account"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/login_signup"
|
||||
style="@style/stepper_nav_button"/>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
68
app/src/main/res/layout/view_collection_fragment.xml
Normal file
68
app/src/main/res/layout/view_collection_fragment.xml
Normal file
@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
layout="@layout/collection_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/activity_margin" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="@dimen/activity_margin"
|
||||
android:text="@string/change_journal_title"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tasks_not_showing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="@dimen/activity_margin"
|
||||
android:visibility="gone"
|
||||
android:text="@string/tasks_not_showing" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/activity_margin">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Stats:" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="right" />
|
||||
</LinearLayout>
|
||||
|
||||
<fragment android:name="com.etesync.syncadapter.ui.etebase.ListEntriesFragment"
|
||||
android:id="@+id/list_entries_container"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
android:src="@drawable/ic_add_light" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -25,6 +25,10 @@
|
||||
android:title="@string/account_show_fingerprint"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item android:id="@+id/invitations"
|
||||
android:title="@string/invitations_title"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item android:id="@+id/delete_account"
|
||||
android:title="@string/account_delete"
|
||||
app:showAsAction="never"/>
|
||||
|
26
app/src/main/res/menu/collection_item_fragment.xml
Normal file
26
app/src/main/res/menu/collection_item_fragment.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright © 2013 – 2016 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
|
||||
-->
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<group android:id="@+id/journal_item_menu_event_invite">
|
||||
<item android:title="@string/calendar_attendees_send_email_action"
|
||||
android:id="@+id/on_send_event_invite"
|
||||
android:icon="@drawable/ic_email_black"
|
||||
app:showAsAction="always" />
|
||||
</group>
|
||||
|
||||
<group android:id="@+id/journal_item_menu_restore">
|
||||
<item android:title="@string/journal_item_restore_action"
|
||||
android:icon="@drawable/ic_restore_black"
|
||||
android:id="@+id/on_restore_item"
|
||||
app:showAsAction="never" />
|
||||
</group>
|
||||
</menu>
|
25
app/src/main/res/menu/fragment_edit_collection.xml
Normal file
25
app/src/main/res/menu/fragment_edit_collection.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright © 2013 – 2016 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
|
||||
-->
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/on_delete"
|
||||
android:icon="@drawable/ic_delete_dark"
|
||||
android:title="@string/delete_collection"
|
||||
app:showAsAction="always" />
|
||||
|
||||
|
||||
<item
|
||||
android:id="@+id/on_save"
|
||||
android:icon="@drawable/ic_save_dark"
|
||||
android:title="@string/create_collection_create"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
27
app/src/main/res/menu/fragment_view_collection.xml
Normal file
27
app/src/main/res/menu/fragment_view_collection.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright © 2013 – 2016 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
|
||||
-->
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item android:title="@string/view_collection_edit"
|
||||
android:id="@+id/on_edit"
|
||||
android:icon="@drawable/ic_edit_dark"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item android:title="@string/view_collection_members"
|
||||
android:id="@+id/on_manage_members"
|
||||
android:icon="@drawable/ic_members_dark"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item android:title="@string/view_collection_import"
|
||||
android:id="@+id/on_import"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
</menu>
|
@ -188,4 +188,4 @@
|
||||
<string name="certificate_notification_connection_security">EteSync: Verbindungs-Sicherheit</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">EteSync hat ein unbekanntes Zertifikat vorgefunden. Wollen Sie ihm vertrauen?</string>
|
||||
<string name="app_settings_log_verbose">Ausführliche Protokollierung</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -197,7 +197,8 @@
|
||||
<string name="login_wrong_username_or_password">Kunne ikke logge inn (sannsynligvis feil brukernavn eller passord).\nHar du registrert deg?</string>
|
||||
<string name="login_view_logs">Vis logger</string>
|
||||
<string name="setting_up_encryption">Setter opp kryptering</string>
|
||||
<string name="setting_up_encryption_content">Vent, setter opp kryptering …</string>
|
||||
<string name="setting_up_encryption_content">Vennligst vent, setter opp kryptering…</string>
|
||||
|
||||
<string name="account_creation_failed">Kunne ikke opprette konto</string>
|
||||
<string name="wrong_encryption_password">Feil krypteringspassord</string>
|
||||
<string name="wrong_encryption_password_content">Mottok en integritetsfeil ved tilgang til kontoen din, noe som mest sannsynlig skyldes at du skrev inn feil krypteringspassord.
|
||||
@ -316,7 +317,7 @@
|
||||
<string name="sync_successfully_contacts" formatted="false">Kontakter endret (%s)</string>
|
||||
<string name="sync_successfully_tasks" formatted="false">Gjøremålene «%s» er endret (%s)</string>
|
||||
<string name="sync_successfully_modified" formatted="false">%s endret.</string>
|
||||
<string name="sync_successfully_modified_full" formatted="false">%s lagt til.\n%s oppdatert.\n%s slettet.</string>
|
||||
<string name="sync_successfully_modified_full" formatted="false">%s oppdatert.\n%s slettet.</string>
|
||||
<string name="sync_journal_readonly">Journalen \"%s\" er skrivebeskyttet</string>
|
||||
<string name="sync_journal_readonly_message">Journalen er skrivebeskyttet, så alle endringene dine (%d) har blitt omgjort.</string>
|
||||
<!-- Calendar invites -->
|
||||
@ -361,4 +362,4 @@
|
||||
<string name="app_settings_prefer_tasksorg">Foretrekk Tasks.org-gjøremålstilbyder</string>
|
||||
<string name="app_settings_sync">Synkroniser</string>
|
||||
<string name="accounts_missing_permissions">Manglende tilganger: %s</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -287,7 +287,7 @@
|
||||
<string name="sync_successfully_calendar" formatted="false">Kalendarz \"%s\" został zmodyfikowany (%s)</string>
|
||||
<string name="sync_successfully_contacts" formatted="false">Kontakty zostały zmodyfikowane (%s)</string>
|
||||
<string name="sync_successfully_modified" formatted="false">%s został zmodyfikowany.</string>
|
||||
<string name="sync_successfully_modified_full" formatted="false">%s został dodany. \n%s został zaktualizowany.\n%s został usunięty.</string>
|
||||
<string name="sync_successfully_modified_full" formatted="false">%s został zaktualizowany.\n%s został usunięty.</string>
|
||||
|
||||
<!-- cert4android -->
|
||||
<string name="certificate_notification_connection_security">EteSync: Bezpieczeństwo połączenia</string>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="sync_successfully">
|
||||
<item quantity="one">%d entry</item>
|
||||
<item quantity="other">%d entries</item>
|
||||
<item quantity="one">%d item</item>
|
||||
<item quantity="other">%d items</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -147,6 +147,7 @@
|
||||
<string name="members_owner_only">Only the owner of this collection (%s) is allowed to view its members.</string>
|
||||
<string name="not_allowed_title">Not Allowed</string>
|
||||
<string name="edit_owner_only">Only the owner of this collection (%s) is allowed to edit it.</string>
|
||||
<string name="edit_owner_only_anon">Only the owner of this collection is allowed to edit it.</string>
|
||||
<string name="members_old_journals_not_allowed">Sharing of old-style journals is not allowed. In order to share this journal, create a new one, and copy its contents over using the \"import\" dialog. If you are experiencing any issues, please contact support.</string>
|
||||
<string name="use_native_apps_title">Did you know?</string>
|
||||
<string name="use_native_apps_body">EteSync seamlessly integrates with Android, so to use it, just use your existing address book and calendar apps!\n\nFor more information, please check out the user guide.</string>
|
||||
@ -160,15 +161,27 @@
|
||||
<string name="collection_members_adding">Adding member</string>
|
||||
<string name="trust_fingerprint_title">Verify security fingerprint</string>
|
||||
<string name="trust_fingerprint_body">Verify %s\'s security fingerprint to ensure the encryption is secure.</string>
|
||||
<string name="collection_members_error_user_not_found">User (%s) not found. Have they setup their encryption password from one of the apps?</string>
|
||||
<string name="collection_members_error_user_not_found">User (%s) not found</string>
|
||||
<string name="collection_members_removing">Removing member</string>
|
||||
<string name="collection_members_remove_error">Error removing member</string>
|
||||
<string name="collection_members_remove_title">Remove member</string>
|
||||
<string name="collection_members_remove">Would you like to revoke %s\'s access?\nPlease be advised that a malicious user would potentially be able to retain access to encryption keys. Please refer to the FAQ for more information.</string>
|
||||
<string name="collection_members_remove_admin">Removing access to admins is currently not supported.</string>
|
||||
<string name="collection_members_no_access">Only admins are allowed to manage collection memberships. Would you like to leave the collection?</string>
|
||||
<string name="collection_members_leave">Leave</string>
|
||||
|
||||
<!-- Invitations -->
|
||||
<string name="invitations_title">Invitations</string>
|
||||
<string name="invitations_loading">Loading invitations...</string>
|
||||
<string name="invitations_list_empty">No invitations</string>
|
||||
<string name="invitations_accept_reject_dialog">Would you like to accept or reject the invitation?</string>
|
||||
<string name="invitations_accept">Accept</string>
|
||||
<string name="invitations_reject">Reject</string>
|
||||
|
||||
<!-- JournalItemActivity -->
|
||||
<string name="about">About</string>
|
||||
<string name="journal_item_tab_main">Main</string>
|
||||
<string name="journal_item_tab_revisions">Revisions</string>
|
||||
<string name="journal_item_tab_raw">Raw</string>
|
||||
<string name="journal_item_attendees">Attendees</string>
|
||||
<string name="journal_item_reminders">Reminders</string>
|
||||
@ -207,8 +220,10 @@
|
||||
|
||||
<!-- AddAccountActivity -->
|
||||
<string name="login_title">Add account</string>
|
||||
<string name="login_username">Username</string>
|
||||
<string name="login_username_error">Valid username required</string>
|
||||
<string name="login_email_address">Email</string>
|
||||
<string name="login_email_address_error">Valid email address required</string>
|
||||
<string name="login_email_address_error">Valid email required</string>
|
||||
<string name="login_password">Password</string>
|
||||
<string name="login_custom_server">EteSync Server URL</string>
|
||||
<string name="login_custom_server_error">Invalid URL found, did you forget to include https://?</string>
|
||||
@ -216,7 +231,7 @@
|
||||
<string name="login_encryption_password">Encryption Password</string>
|
||||
<string name="login_encryption_set_new_password">Please set your encryption password below, and make sure you got it right, as it *can\'t* be recovered if lost!</string>
|
||||
<string name="login_encryption_enter_password">You are logged in as \"%s\". Please enter your encryption password to continue, or log out from the side menu.</string>
|
||||
<string name="login_encryption_check_password">* Please double-check the password, as it can\'t be recovered if wrong!</string>
|
||||
<string name="login_encryption_check_password">* Please make sure you remember your password, as it can\'t be recovered if lost!</string>
|
||||
<string name="login_encryption_extra_info">* This password is used to encrypt your data, unlike the previous one, which is used to log into the service.\nYou are asked to choose a separate encryption password for security reasons. For more information, please refer to the FAQ at: %s</string>
|
||||
<string name="login_password_required">Password required</string>
|
||||
<string name="login_login">Log In</string>
|
||||
@ -229,8 +244,6 @@
|
||||
<string name="login_service_details_description">This is your login password, *not* your encryption password!</string>
|
||||
<string name="login_forgot_password">Forgot password?</string>
|
||||
|
||||
<string name="login_configuration_detection">Configuration detection</string>
|
||||
<string name="login_querying_server">Please wait, querying server…</string>
|
||||
<string name="login_wrong_username_or_password">Couldn\'t authenticate (probably wrong username or password).\nHave you registered?</string>
|
||||
<string name="login_view_logs">View logs</string>
|
||||
|
||||
@ -241,6 +254,9 @@
|
||||
<string name="wrong_encryption_password">Wrong encryption password</string>
|
||||
<string name="wrong_encryption_password_content">Got an integrity error while accessing your account, which most likely means you put in the wrong encryption password.\nPlease note that the username is case sensitive, so please also try different capitalizations, for example make the first character uppercase.\n\nError: %s</string>
|
||||
|
||||
<string name="signup_title">Enter Signup Details</string>
|
||||
<string name="signup_password_restrictions">Password should be at least 8 characters long</string>
|
||||
|
||||
<!-- ChangeEncryptionPasswordActivity -->
|
||||
<string name="change_encryption_password_title">Change Encryption Password</string>
|
||||
<string name="change_encryption_password_extra_info">Please don\'t use this tool if you believe your encryption password has been compromised. Contact support instead.</string>
|
||||
@ -332,8 +348,8 @@
|
||||
<string name="delete_collection_deleting_collection">Deleting collection</string>
|
||||
|
||||
<!-- JournalViewer -->
|
||||
<string name="journal_entries_list_empty">Journal is empty.\n(Maybe it\'s still syncing?)</string>
|
||||
<string name="journal_entries_loading">Loading journal entries...</string>
|
||||
<string name="journal_entries_list_empty">Collection is empty.\nMaybe it\'s still syncing?</string>
|
||||
<string name="journal_entries_loading">Loading change log entries...</string>
|
||||
|
||||
<!-- ExceptionInfoFragment -->
|
||||
<string name="exception">An error has occurred.</string>
|
||||
@ -347,6 +363,7 @@
|
||||
<string name="debug_info_more_data_shared">Clicking share will open the email app with the data below, as well as some additional debug information, attached. It may contain some sensitive information, so please review it before sending.</string>
|
||||
<string name="sync_error_permissions">EteSync permissions</string>
|
||||
<string name="sync_error_permissions_text">Additional permissions required</string>
|
||||
<string name="sync_error_generic">Sync failed (%s)</string>
|
||||
<string name="sync_error_calendar">Calendar sync failed (%s)</string>
|
||||
<string name="sync_error_contacts">Contacts sync failed (%s)</string>
|
||||
<string name="sync_error_tasks">Tasks sync failed (%s)</string>
|
||||
@ -356,6 +373,7 @@
|
||||
<string name="sync_error_unavailable">Could not connect to server while %s</string>
|
||||
<string name="sync_error_local_storage">Database error while %s</string>
|
||||
<string name="sync_error_journal_readonly">Journal is read only</string>
|
||||
<string name="sync_error_permission_denied">Permission denied: %s</string>
|
||||
<string name="sync_phase_prepare">preparing synchronization</string>
|
||||
<string name="sync_phase_journals">syncronizing journals</string>
|
||||
<string name="sync_phase_prepare_fetch">preparing for fetch</string>
|
||||
@ -372,7 +390,7 @@
|
||||
<string name="sync_successfully_contacts" formatted="false">Contacts modified (%s)</string>
|
||||
<string name="sync_successfully_tasks" formatted="false">Tasks \"%s\" modified (%s)</string>
|
||||
<string name="sync_successfully_modified" formatted="false">%s modified.</string>
|
||||
<string name="sync_successfully_modified_full" formatted="false">%s added.\n%s updated.\n%s deleted.</string>
|
||||
<string name="sync_successfully_modified_full" formatted="false">%s updated.\n%s deleted.</string>
|
||||
<string name="sync_journal_readonly">Journal \"%s\" is read only</string>
|
||||
<string name="sync_journal_readonly_message">The journal is read only so all of your changes (%d) have been reverted.</string>
|
||||
|
||||
|
@ -18,25 +18,12 @@
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/settings_encryption">
|
||||
|
||||
<Preference
|
||||
android:title="@string/settings_encryption_password"
|
||||
android:key="encryption_password"
|
||||
android:summary="@string/settings_encryption_password_summary"
|
||||
android:persistent="false"
|
||||
/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/settings_sync">
|
||||
<EditTextPreference
|
||||
<Preference
|
||||
android:key="password"
|
||||
android:title="@string/settings_password"
|
||||
android:persistent="false"
|
||||
android:inputType="textPassword"
|
||||
android:summary="@string/settings_password_summary"
|
||||
android:dialogTitle="@string/settings_enter_password" />
|
||||
android:title="@string/settings_encryption_password"
|
||||
android:summary="@string/settings_encryption_password_summary"
|
||||
/>
|
||||
|
||||
<ListPreference
|
||||
android:key="sync_interval"
|
||||
|
64
app/src/main/res/xml/settings_account_legacy.xml
Normal file
64
app/src/main/res/xml/settings_account_legacy.xml
Normal file
@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2013 – 2015 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
|
||||
-->
|
||||
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory android:title="@string/settings_manage_account">
|
||||
|
||||
<Preference
|
||||
android:key="manage_account"
|
||||
android:title="@string/settings_account_dashboard"
|
||||
android:persistent="false"
|
||||
android:summary="@string/settings_manage_account_summary" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/settings_encryption">
|
||||
|
||||
<Preference
|
||||
android:title="@string/settings_encryption_password"
|
||||
android:key="encryption_password"
|
||||
android:summary="@string/settings_encryption_password_summary"
|
||||
android:persistent="false"
|
||||
/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/settings_sync">
|
||||
<EditTextPreference
|
||||
android:key="password"
|
||||
android:title="@string/settings_password"
|
||||
android:persistent="false"
|
||||
android:inputType="textPassword"
|
||||
android:summary="@string/settings_password_summary"
|
||||
android:dialogTitle="@string/settings_enter_password" />
|
||||
|
||||
<ListPreference
|
||||
android:key="sync_interval"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_sync_interval"
|
||||
android:entries="@array/settings_sync_interval_names"
|
||||
android:entryValues="@array/settings_sync_interval_seconds" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="sync_wifi_only"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_sync_wifi_only"
|
||||
android:summaryOn="@string/settings_sync_wifi_only_on"
|
||||
android:summaryOff="@string/settings_sync_wifi_only_off"
|
||||
/>
|
||||
|
||||
<EditTextPreference
|
||||
android:key="sync_wifi_only_ssid"
|
||||
android:dependency="sync_wifi_only"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_sync_wifi_only_ssid"
|
||||
android:dialogMessage="@string/settings_sync_wifi_only_ssid_message"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
Loading…
Reference in New Issue
Block a user