Invitations: implement invitations handling.

pull/131/head
Tom Hacohen 4 years ago
parent b11ece37d5
commit bf1155d0b8

@ -127,7 +127,7 @@
<!-- Address book account -->
<service
android:name=".syncadapter.NullAuthenticatorService"
android:exported="true"> <!-- Since Android 11, this must be true so that Google Contacts shows the address book accounts -->
android:exported="true"> <!-- Since Android 11, this must be true so that Google Contacts shows the address book accounts -->
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
@ -237,6 +237,10 @@
android:name=".ui.etebase.CollectionActivity"
android:exported="false"
/>
<activity
android:name=".ui.etebase.InvitationsActivity"
android:exported="false"
/>
<activity
android:name=".ui.ViewCollectionActivity"
android:exported="false"

@ -45,6 +45,7 @@ import com.etesync.syncadapter.resource.LocalAddressBook
import com.etesync.syncadapter.resource.LocalCalendar
import com.etesync.syncadapter.syncadapter.requestSync
import com.etesync.syncadapter.ui.etebase.CollectionActivity
import com.etesync.syncadapter.ui.etebase.InvitationsActivity
import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment
import com.etesync.syncadapter.utils.HintManager
import com.etesync.syncadapter.utils.ShowcaseBuilder
@ -156,6 +157,10 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.activity_account, menu)
if (settings.isLegacy) {
val invitations = menu.findItem(R.id.invitations)
invitations.setVisible(false)
}
return true
}
@ -185,6 +190,10 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
.setPositiveButton(android.R.string.yes) { _, _ -> }.create()
dialog.show()
}
R.id.invitations -> {
val intent = InvitationsActivity.newIntent(this, account)
startActivity(intent)
}
else -> return super.onOptionsItemSelected(item)
}
return true

@ -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,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>

@ -25,6 +25,10 @@
android:title="@string/account_show_fingerprint"
app:showAsAction="ifRoom"/>
<item android:id="@+id/invitations"
android:title="@string/invitations_title"
app:showAsAction="never"/>
<item android:id="@+id/delete_account"
android:title="@string/account_delete"
app:showAsAction="never"/>

@ -168,6 +168,14 @@
<string name="collection_members_remove">Would you like to revoke %s\'s access?\nPlease be advised that a malicious user would potentially be able to retain access to encryption keys. Please refer to the FAQ for more information.</string>
<string name="collection_members_remove_admin">Removing access to admins is currently not supported.</string>
<!-- Invitations -->
<string name="invitations_title">Invitations</string>
<string name="invitations_loading">Loading invitations...</string>
<string name="invitations_list_empty">No invitations</string>
<string name="invitations_accept_reject_dialog">Would you like to accept or reject the invitation?</string>
<string name="invitations_accept">Accept</string>
<string name="invitations_reject">Reject</string>
<!-- JournalItemActivity -->
<string name="about">About</string>
<string name="journal_item_tab_main">Main</string>

Loading…
Cancel
Save