From 63a8bf91a99cbb4ff39115b25f500991aad1151e Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 27 Aug 2020 09:27:06 +0300 Subject: [PATCH] ViewCollection: add a basic etebase collection viewing activity. --- app/src/main/AndroidManifest.xml | 4 + .../etesync/syncadapter/EtebaseLocalCache.kt | 6 +- .../etesync/syncadapter/ui/AccountActivity.kt | 7 +- .../ui/etebase/CollectionActivity.kt | 103 +++++++++++++ .../ui/etebase/ListEntriesFragment.kt | 140 ++++++++++++++++++ .../ui/etebase/ViewCollectionFragment.kt | 137 +++++++++++++++++ .../layout/etebase_collection_activity.xml | 11 ++ .../res/layout/view_collection_fragment.xml | 68 +++++++++ .../res/menu/fragment_view_collection.xml | 27 ++++ 9 files changed, 499 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt create mode 100644 app/src/main/res/layout/etebase_collection_activity.xml create mode 100644 app/src/main/res/layout/view_collection_fragment.xml create mode 100644 app/src/main/res/menu/fragment_view_collection.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 45c699a0..d40e92c1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -233,6 +233,10 @@ android:exported="false" android:parentActivityName=".ui.AccountsActivity"> + 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? diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt new file mode 100644 index 00000000..b762f6d1 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionActivity.kt @@ -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() + + 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>() + + 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) -> Unit) = + cachedItems.observe(owner, observer) +} \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt new file mode 100644 index 00000000..eed06aa4 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt @@ -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? = 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(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(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(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(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(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(R.id.action) as ImageView + if (item.item.isDeleted) { + action.setImageResource(R.drawable.action_delete) + } else { + action.setImageResource(R.drawable.action_change) + } + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt new file mode 100644 index 00000000..6e89f503 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ViewCollectionFragment.kt @@ -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(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(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(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(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(R.id.description) + desc.text = meta.description + + val owner = container.findViewById(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(R.id.stats) + container.findViewById(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" + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/etebase_collection_activity.xml b/app/src/main/res/layout/etebase_collection_activity.xml new file mode 100644 index 00000000..b500265d --- /dev/null +++ b/app/src/main/res/layout/etebase_collection_activity.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/view_collection_fragment.xml b/app/src/main/res/layout/view_collection_fragment.xml new file mode 100644 index 00000000..1b940572 --- /dev/null +++ b/app/src/main/res/layout/view_collection_fragment.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/fragment_view_collection.xml b/app/src/main/res/menu/fragment_view_collection.xml new file mode 100644 index 00000000..42ff6209 --- /dev/null +++ b/app/src/main/res/menu/fragment_view_collection.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + \ No newline at end of file