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"?>
|
||||
<resources>
|
||||
<plurals name="sync_successfully">
|
||||
<item quantity="one">%d entry</item>
|
||||
<item quantity="other">%d entries</item>
|
||||
<item quantity="one">%d item</item>
|
||||
<item quantity="other">%d items</item>
|
||||
</plurals>
|
||||
</resources>
|
Loading…
Reference in new issue