ViewCollection: add a basic etebase collection viewing activity.

pull/131/head
Tom Hacohen 4 years ago
parent 481dcc1944
commit 63a8bf91a9

@ -233,6 +233,10 @@
android:exported="false"
android:parentActivityName=".ui.AccountsActivity">
</activity>
<activity
android:name=".ui.etebase.CollectionActivity"
android:exported="false"
/>
<activity
android:name=".ui.ViewCollectionActivity"
android:exported="false"

@ -109,7 +109,7 @@ class EtebaseLocalCache private constructor(context: Context, username: String)
val content = itemFile.readBytes()
itemMgr.cacheLoad(content)
}.filter { withDeleted || !it.isDeleted }.map {
CachedItem(it, it.meta)
CachedItem(it, it.meta, it.contentString)
}
}
@ -121,7 +121,7 @@ class EtebaseLocalCache private constructor(context: Context, username: String)
}
val content = itemFile.readBytes()
return itemMgr.cacheLoad(content).let {
CachedItem(it, it.meta)
CachedItem(it, it.meta, it.contentString)
}
}
@ -162,4 +162,4 @@ class EtebaseLocalCache private constructor(context: Context, username: String)
data class CachedCollection(val col: Collection, val meta: CollectionMetadata)
data class CachedItem(val item: Item, val meta: ItemMetadata)
data class CachedItem(val item: Item, val meta: ItemMetadata, val content: String)

@ -43,6 +43,7 @@ import com.etesync.syncadapter.model.ServiceEntity
import com.etesync.syncadapter.resource.LocalAddressBook
import com.etesync.syncadapter.resource.LocalCalendar
import com.etesync.syncadapter.syncadapter.requestSync
import com.etesync.syncadapter.ui.etebase.CollectionActivity
import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment
import com.etesync.syncadapter.utils.HintManager
import com.etesync.syncadapter.utils.ShowcaseBuilder
@ -71,7 +72,11 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
val adapter = list.adapter as ArrayAdapter<*>
val info = adapter.getItem(position) as CollectionListItemInfo
startActivity(ViewCollectionActivity.newIntent(this@AccountActivity, account, info.legacyInfo!!))
if (settings.isLegacy) {
startActivity(ViewCollectionActivity.newIntent(this@AccountActivity, account, info.legacyInfo!!))
} else {
startActivity(CollectionActivity.newIntent(this@AccountActivity, account, info.uid))
}
}
private val formattedFingerprint: String?

@ -0,0 +1,103 @@
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.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.observe
import com.etebase.client.CollectionManager
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 lateinit var colUid: String
private val model: AccountCollectionViewModel by viewModels()
private val itemsModel: ItemsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
account = intent.extras!!.getParcelable(EXTRA_ACCOUNT)!!
colUid = intent.extras!!.getString(EXTRA_COLLECTION_UID)!!
setContentView(R.layout.etebase_collection_activity)
if (savedInstanceState == null) {
model.loadCollection(this, account, colUid)
model.observe(this) {
itemsModel.loadItems(it)
}
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, ViewCollectionFragment())
.commit()
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
companion object {
private val EXTRA_ACCOUNT = "account"
private val EXTRA_COLLECTION_UID = "collectionUid"
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
}
}
}
class AccountCollectionViewModel : ViewModel() {
private val collection = MutableLiveData<AccountCollectionHolder>()
fun loadCollection(context: Context, account: Account, colUid: String) {
doAsync {
val settings = AccountSettings(context, account)
val etebaseLocalCache = EtebaseLocalCache.getInstance(context, account.name)
val etebase = EtebaseLocalCache.getEtebase(context, HttpClient.sharedClient, settings)
val colMgr = etebase.collectionManager
val cachedCollection = synchronized(etebaseLocalCache) {
etebaseLocalCache.collectionGet(colMgr, colUid)!!
}
uiThread {
collection.value = AccountCollectionHolder(
etebaseLocalCache,
etebase,
colMgr,
cachedCollection
)
}
}
}
fun observe(owner: LifecycleOwner, observer: (AccountCollectionHolder) -> Unit) =
collection.observe(owner, observer)
}
data class AccountCollectionHolder(val etebaseLocalCache: EtebaseLocalCache, val etebase: com.etebase.client.Account, val colMgr: CollectionManager, val cachedCollection: CachedCollection)
class ItemsViewModel : ViewModel() {
private val cachedItems = MutableLiveData<List<CachedItem>>()
fun loadItems(accountCollectionHolder: AccountCollectionHolder) {
doAsync {
val col = accountCollectionHolder.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)
}

@ -0,0 +1,140 @@
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.fragment.app.ListFragment
import androidx.fragment.app.activityViewModels
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.CachedItem
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
import java.text.SimpleDateFormat
import java.util.concurrent.Future
class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener {
private val model: AccountCollectionViewModel by activityViewModels()
private val itemsModel: ItemsViewModel by activityViewModels()
private var asyncTask: Future<Unit>? = 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)
model.observe(this) { col ->
itemsModel.observe(this) {
val entries = it.sortedByDescending { item ->
item.meta.mtime ?: 0
}
val listAdapter = EntriesListAdapter(requireContext(), col.cachedCollection)
setListAdapter(listAdapter)
listAdapter.addAll(entries)
emptyTextView!!.text = getString(R.string.journal_entries_list_empty)
}
}
listView.onItemClickListener = this
}
override fun onDestroyView() {
super.onDestroyView()
if (asyncTask != null)
asyncTask!!.cancel(true)
}
override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val item = listAdapter?.getItem(position) as CachedItem
Toast.makeText(context, "Clicked ${item.item.uid}", Toast.LENGTH_LONG).show()
// startActivity(JournalItemActivity.newIntent(requireContext(), account, info, entry.content))
}
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,137 @@
package com.etesync.syncadapter.ui.etebase
import android.content.Context
import android.content.DialogInterface
import android.graphics.Color.parseColor
import android.os.Bundle
import android.view.*
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
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 model: AccountCollectionViewModel by activityViewModels()
private val itemsModel: ItemsViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val ret = super.onCreateView(inflater, container, savedInstanceState)
inflater.inflate(R.layout.view_collection_fragment, container)
setHasOptionsMenu(true)
if (savedInstanceState == null) {
model.observe(this) {
if (container != null) {
initUi(inflater, container, it)
}
}
}
return ret
}
override fun onAttach(context: Context) {
super.onAttach(context)
model.observe(this) {
(activity as? BaseActivity?)?.supportActionBar?.title = it.cachedCollection.meta.name
}
}
private fun initUi(inflater: LayoutInflater, container: ViewGroup, collectionHolder: AccountCollectionHolder) {
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 = collectionHolder.cachedCollection.col
val meta = collectionHolder.cachedCollection.meta
val isAdmin = col.accessLevel == "adm"
val colorSquare = container.findViewById<View>(R.id.color)
val color = if (!meta.color.isNullOrBlank()) parseColor(meta.color) else LocalCalendar.defaultColor
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 {
when (item.itemId) {
R.id.on_edit -> {
Toast.makeText(context, "Edit", Toast.LENGTH_LONG).show()
}
R.id.on_manage_members -> {
Toast.makeText(context, "Manage", Toast.LENGTH_LONG).show()
}
R.id.on_import -> {
Toast.makeText(context, "Import", Toast.LENGTH_LONG).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,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>

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2013 2016 Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/view_collection_edit"
android:id="@+id/on_edit"
android:icon="@drawable/ic_edit_dark"
app:showAsAction="always" />
<item android:title="@string/view_collection_members"
android:id="@+id/on_manage_members"
android:icon="@drawable/ic_members_dark"
app:showAsAction="ifRoom"/>
<item android:title="@string/view_collection_import"
android:id="@+id/on_import"
app:showAsAction="never"/>
</menu>
Loading…
Cancel
Save