mirror of https://github.com/etesync/android
commit
d4ef9f7fe3
@ -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)
|
@ -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,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"
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<plurals name="sync_successfully">
|
<plurals name="sync_successfully">
|
||||||
<item quantity="one">%d entry</item>
|
<item quantity="one">%d item</item>
|
||||||
<item quantity="other">%d entries</item>
|
<item quantity="other">%d items</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in new issue