diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b0600fb8..e7c9dc60 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -184,6 +184,7 @@
android:parentActivityName=".ui.AccountsActivity">
+
diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java
index 92246cca..0ae3f440 100644
--- a/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java
+++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java
@@ -115,6 +115,12 @@ public class Crypto {
public static byte[] getKeyFingerprint(byte[] pubkey) {
return sha256(pubkey);
}
+
+ public static String getPrettyKeyFingerprint(byte[] pubkey) {
+ byte[] fingerprint = Crypto.AsymmetricCryptoManager.getKeyFingerprint(pubkey);
+ String fingerprintString = Hex.toHexString(fingerprint).toLowerCase();
+ return fingerprintString.replaceAll("(.{4})", "$1 ");
+ }
}
public static class CryptoManager {
@@ -125,6 +131,7 @@ public class Crypto {
private final byte version;
private byte[] cipherKey;
private byte[] hmacKey;
+ private byte[] derivedKey;
private void setDerivedKey(byte[] derivedKey) {
cipherKey = hmac256("aes".getBytes(Charsets.UTF_8), derivedKey);
@@ -133,14 +140,13 @@ public class Crypto {
public CryptoManager(int version, AsymmetricKeyPair keyPair, byte[] encryptedKey) {
Crypto.AsymmetricCryptoManager cryptoManager = new Crypto.AsymmetricCryptoManager(keyPair);
- byte[] derivedKey = cryptoManager.decrypt(encryptedKey);
+ derivedKey = cryptoManager.decrypt(encryptedKey);
this.version = (byte) version;
setDerivedKey(derivedKey);
}
public CryptoManager(int version, @NonNull String keyBase64, @NonNull String salt) throws Exceptions.IntegrityException, Exceptions.VersionTooNewException {
- byte[] derivedKey;
if (version > Byte.MAX_VALUE) {
throw new Exceptions.IntegrityException("Version is out of range.");
} else if (version > Constants.CURRENT_VERSION) {
@@ -238,6 +244,11 @@ public class Crypto {
hmac.doFinal(ret, 0);
return ret;
}
+
+ public byte[] getEncryptedKey(AsymmetricKeyPair keyPair, byte[] publicKey) {
+ AsymmetricCryptoManager cryptoManager = new AsymmetricCryptoManager(keyPair);
+ return cryptoManager.encrypt(publicKey, derivedKey);
+ }
}
static String sha256(String base) {
diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java
index 522f311a..41f0c2fb 100644
--- a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java
+++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java
@@ -144,6 +144,12 @@ public class JournalManager extends BaseManager {
super();
}
+ public static Journal fakeWithUid(String uid) {
+ Journal ret = new Journal();
+ ret.setUid(uid);
+ return ret;
+ }
+
public Journal(Crypto.CryptoManager crypto, String content, String uid) {
super(crypto, content, uid);
hmac = calculateHmac(crypto);
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java
index c89096e9..9670b1bb 100644
--- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java
+++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java
@@ -217,9 +217,7 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
AccountSettings settings = null;
try {
settings = new AccountSettings(this, account);
- byte[] fingerprint = Crypto.AsymmetricCryptoManager.getKeyFingerprint(settings.getKeyPair().getPublicKey());
- String fingerprintString = Hex.toHexString(fingerprint).toLowerCase();
- return fingerprintString.replaceAll("(.{4})", "$1 ");
+ return Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(settings.getKeyPair().getPublicKey());
} catch (Exception e) {
e.printStackTrace();
return null;
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java
new file mode 100644
index 00000000..96547d02
--- /dev/null
+++ b/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java
@@ -0,0 +1,174 @@
+package com.etesync.syncadapter.ui;
+
+import android.accounts.Account;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+
+import com.etesync.syncadapter.AccountSettings;
+import com.etesync.syncadapter.Constants;
+import com.etesync.syncadapter.HttpClient;
+import com.etesync.syncadapter.InvalidAccountException;
+import com.etesync.syncadapter.R;
+import com.etesync.syncadapter.journalmanager.Crypto;
+import com.etesync.syncadapter.journalmanager.JournalManager;
+import com.etesync.syncadapter.journalmanager.UserInfoManager;
+import com.etesync.syncadapter.model.CollectionInfo;
+
+import lombok.RequiredArgsConstructor;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+
+public class AddMemberFragment extends DialogFragment {
+ final static private String KEY_MEMBER = "memberEmail";
+ private Account account;
+ private AccountSettings settings;
+ private OkHttpClient httpClient;
+ private HttpUrl remote;
+ private CollectionInfo info;
+ private String memberEmail;
+ private byte[] memberPubKey;
+
+ public static AddMemberFragment newInstance(Account account, CollectionInfo info, String email) {
+ AddMemberFragment frag = new AddMemberFragment();
+ Bundle args = new Bundle(1);
+ args.putParcelable(Constants.KEY_ACCOUNT, account);
+ args.putSerializable(Constants.KEY_COLLECTION_INFO, info);
+ args.putString(KEY_MEMBER, email);
+ frag.setArguments(args);
+ return frag;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ account = getArguments().getParcelable(Constants.KEY_ACCOUNT);
+ info = (CollectionInfo) getArguments().getSerializable(Constants.KEY_COLLECTION_INFO);
+ memberEmail = getArguments().getString(KEY_MEMBER);
+ try {
+ settings = new AccountSettings(getContext(), account);
+ httpClient = HttpClient.create(getContext(), account);
+ } catch (InvalidAccountException e) {
+ e.printStackTrace();
+ }
+ remote = HttpUrl.get(settings.getUri());
+
+ new MemberAdd().execute();
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ ProgressDialog progress = new ProgressDialog(getContext());
+ progress.setTitle(R.string.collection_members_adding);
+ progress.setMessage(getString(R.string.please_wait));
+ progress.setIndeterminate(true);
+ progress.setCanceledOnTouchOutside(false);
+ setCancelable(false);
+ return progress;
+ }
+
+ private class MemberAdd extends AsyncTask {
+ @Override
+ protected AddResult doInBackground(Void... voids) {
+ try {
+ UserInfoManager userInfoManager = new UserInfoManager(httpClient, remote);
+
+ Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid);
+
+ memberPubKey = userInfoManager.get(crypto, memberEmail).getPubkey();
+ return new AddResult(null);
+ } catch (Exception e) {
+ return new AddResult(e);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(AddResult result) {
+ if (result.throwable == null) {
+ String fingerprint = Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(memberPubKey);
+
+ new AlertDialog.Builder(getActivity())
+ .setIcon(R.drawable.ic_info_dark)
+ .setTitle(R.string.trust_fingerprint_title)
+ .setMessage(fingerprint)
+ .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new MemberAddSecond().execute();
+ }
+ })
+ .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ }
+ }).show();
+ } else {
+ new AlertDialog.Builder(getActivity())
+ .setIcon(R.drawable.ic_error_dark)
+ .setTitle(R.string.collection_members_add_error)
+ .setMessage(result.throwable.getMessage())
+ .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ }
+ }).show();
+ dismiss();
+ }
+ }
+
+ @RequiredArgsConstructor
+ class AddResult {
+ final Throwable throwable;
+ }
+ }
+
+ private class MemberAddSecond extends AsyncTask {
+ @Override
+ protected AddResultSecond doInBackground(Void... voids) {
+ try {
+ JournalManager journalsManager = new JournalManager(httpClient, remote);
+
+ JournalManager.Journal journal = JournalManager.Journal.fakeWithUid(info.uid);
+ Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid);
+
+ byte[] encryptedKey = crypto.getEncryptedKey(settings.getKeyPair(), memberPubKey);
+ JournalManager.Member member = new JournalManager.Member(memberEmail, encryptedKey);
+ journalsManager.addMember(journal, member);
+ return new AddResultSecond(null);
+ } catch (Exception e) {
+ return new AddResultSecond(e);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(AddResultSecond result) {
+ if (result.throwable == null) {
+ ((Refreshable) getActivity()).refresh();
+ } else {
+ new AlertDialog.Builder(getActivity())
+ .setIcon(R.drawable.ic_error_dark)
+ .setTitle(R.string.collection_members_add_error)
+ .setMessage(result.throwable.getMessage())
+ .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ }
+ }).show();
+ }
+ dismiss();
+ }
+
+ @RequiredArgsConstructor
+ class AddResultSecond {
+ final Throwable throwable;
+ }
+ }
+}
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.java
new file mode 100644
index 00000000..eccc77fe
--- /dev/null
+++ b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.java
@@ -0,0 +1,153 @@
+package com.etesync.syncadapter.ui;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.text.InputType;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.etesync.syncadapter.App;
+import com.etesync.syncadapter.R;
+import com.etesync.syncadapter.model.CollectionInfo;
+import com.etesync.syncadapter.model.JournalEntity;
+import com.etesync.syncadapter.resource.LocalCalendar;
+
+import io.requery.Persistable;
+import io.requery.sql.EntityDataStore;
+
+public class CollectionMembersActivity extends AppCompatActivity implements Refreshable {
+ public final static String EXTRA_ACCOUNT = "account",
+ EXTRA_COLLECTION_INFO = "collectionInfo";
+
+ private Account account;
+ private JournalEntity journalEntity;
+ private CollectionMembersListFragment listFragment;
+ protected CollectionInfo info;
+
+ public static Intent newIntent(Context context, Account account, CollectionInfo info) {
+ Intent intent = new Intent(context, CollectionMembersActivity.class);
+ intent.putExtra(CollectionMembersActivity.EXTRA_ACCOUNT, account);
+ intent.putExtra(CollectionMembersActivity.EXTRA_COLLECTION_INFO, info);
+ return intent;
+ }
+
+ @Override
+ public void refresh() {
+ EntityDataStore data = ((App) getApplicationContext()).getData();
+
+ journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid);
+ if ((journalEntity == null) || journalEntity.isDeleted()) {
+ finish();
+ return;
+ }
+
+ info = journalEntity.getInfo();
+
+ setTitle(R.string.collection_members_title);
+
+ final View colorSquare = findViewById(R.id.color);
+ if (info.type == CollectionInfo.Type.CALENDAR) {
+ if (info.color != null) {
+ colorSquare.setBackgroundColor(info.color);
+ } else {
+ colorSquare.setBackgroundColor(LocalCalendar.defaultColor);
+ }
+ } else {
+ colorSquare.setVisibility(View.GONE);
+ }
+ findViewById(R.id.progressBar).setVisibility(View.GONE);
+
+ final TextView title = (TextView) findViewById(R.id.display_name);
+ title.setText(info.displayName);
+
+ final TextView desc = (TextView) findViewById(R.id.description);
+ desc.setText(info.description);
+
+ if (listFragment != null) {
+ listFragment.refresh();
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.view_collection_members);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ account = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT);
+ info = (CollectionInfo) getIntent().getExtras().getSerializable(EXTRA_COLLECTION_INFO);
+
+ refresh();
+
+ // We refresh before this, so we don't refresh the list before it was fully created.
+ if (savedInstanceState == null) {
+ listFragment = CollectionMembersListFragment.newInstance(account, info);
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.list_entries_container, listFragment)
+ .commit();
+ }
+ }
+
+ public void onAddMemberClicked(View v) {
+ final EditText input = new EditText(this);
+ input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
+ AlertDialog.Builder dialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.collection_members_add)
+ .setIcon(R.drawable.ic_account_add_dark)
+ .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ DialogFragment frag = AddMemberFragment.newInstance(account, info, input.getText().toString());
+ frag.show(getSupportFragmentManager(), null);
+ }
+ })
+ .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ }
+ });
+ dialog.setView(input);
+ dialog.show();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ if (!getSupportFragmentManager().popBackStackImmediate()) {
+ finish();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ App app = (App) getApplicationContext();
+ if (app.getCertManager() != null)
+ app.getCertManager().appInForeground = true;
+
+ refresh();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ App app = (App) getApplicationContext();
+ if (app.getCertManager() != null)
+ app.getCertManager().appInForeground = false;
+ }
+}
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.java
new file mode 100644
index 00000000..73559d7b
--- /dev/null
+++ b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.java
@@ -0,0 +1,164 @@
+package com.etesync.syncadapter.ui;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.ListFragment;
+import android.support.v7.app.AlertDialog;
+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 com.etesync.syncadapter.AccountSettings;
+import com.etesync.syncadapter.App;
+import com.etesync.syncadapter.Constants;
+import com.etesync.syncadapter.HttpClient;
+import com.etesync.syncadapter.R;
+import com.etesync.syncadapter.journalmanager.JournalManager;
+import com.etesync.syncadapter.model.CollectionInfo;
+import com.etesync.syncadapter.model.EntryEntity;
+import com.etesync.syncadapter.model.JournalEntity;
+import com.etesync.syncadapter.model.JournalModel;
+
+import java.util.List;
+
+import io.requery.Persistable;
+import io.requery.sql.EntityDataStore;
+import lombok.RequiredArgsConstructor;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+
+public class CollectionMembersListFragment extends ListFragment implements AdapterView.OnItemClickListener, Refreshable {
+ private EntityDataStore data;
+ private Account account;
+ private CollectionInfo info;
+ private JournalEntity journalEntity;
+
+ private TextView emptyTextView;
+
+ public static CollectionMembersListFragment newInstance(Account account, CollectionInfo info) {
+ CollectionMembersListFragment frag = new CollectionMembersListFragment();
+ Bundle args = new Bundle(1);
+ args.putParcelable(Constants.KEY_ACCOUNT, account);
+ args.putSerializable(Constants.KEY_COLLECTION_INFO, info);
+ frag.setArguments(args);
+ return frag;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ data = ((App) getContext().getApplicationContext()).getData();
+ account = getArguments().getParcelable(Constants.KEY_ACCOUNT);
+ info = (CollectionInfo) getArguments().getSerializable(Constants.KEY_COLLECTION_INFO);
+ journalEntity = JournalModel.Journal.fetch(data, info.getServiceEntity(data), info.uid);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View 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 = (TextView) view.findViewById(android.R.id.empty);
+ return view;
+ }
+
+ @Override
+ public void refresh() {
+ new JournalMembersFetch().execute();
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ refresh();
+
+ getListView().setOnItemClickListener(this);
+ }
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ final JournalManager.Member member = (JournalManager.Member) getListAdapter().getItem(position);
+
+ new AlertDialog.Builder(getActivity())
+ .setIcon(R.drawable.ic_info_dark)
+ .setTitle(R.string.collection_members_remove_title)
+ .setMessage(getString(R.string.collection_members_remove, member.getUser()))
+ .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ DialogFragment frag = RemoveMemberFragment.newInstance(account, info, member.getUser());
+ frag.show(getFragmentManager(), null);
+ }
+ })
+ .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ }
+ }).show();
+ }
+
+ class MembersListAdapter extends ArrayAdapter {
+ MembersListAdapter(Context context) {
+ super(context, R.layout.collection_members_list_item);
+ }
+
+ @Override
+ @NonNull
+ public View getView(int position, View v, @NonNull ViewGroup parent) {
+ if (v == null)
+ v = LayoutInflater.from(getContext()).inflate(R.layout.collection_members_list_item, parent, false);
+
+ JournalManager.Member member = getItem(position);
+
+ TextView tv = (TextView) v.findViewById(R.id.title);
+ tv.setText(member.getUser());
+ return v;
+ }
+ }
+
+ private class JournalMembersFetch extends AsyncTask {
+ @Override
+ protected MembersResult doInBackground(Void... voids) {
+ try {
+ OkHttpClient httpClient = HttpClient.create(getContext(), account);
+ AccountSettings settings = new AccountSettings(getContext(), account);
+ JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri()));
+
+ JournalManager.Journal journal = JournalManager.Journal.fakeWithUid(journalEntity.getUid());
+ return new MembersResult(journalsManager.listMembers(journal), null);
+ } catch (Exception e) {
+ return new MembersResult(null, e);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(MembersResult result) {
+ if (result.throwable == null) {
+ MembersListAdapter listAdapter = new MembersListAdapter(getContext());
+ setListAdapter(listAdapter);
+
+ listAdapter.addAll(result.members);
+
+ emptyTextView.setText(R.string.collection_members_list_empty);
+ } else {
+ emptyTextView.setText(result.throwable.getLocalizedMessage());
+ }
+ }
+
+ @RequiredArgsConstructor
+ class MembersResult {
+ final List members;
+ final Throwable throwable;
+ }
+ }
+}
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.java
new file mode 100644
index 00000000..a8191238
--- /dev/null
+++ b/app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.java
@@ -0,0 +1,116 @@
+package com.etesync.syncadapter.ui;
+
+import android.accounts.Account;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+
+import com.etesync.syncadapter.AccountSettings;
+import com.etesync.syncadapter.Constants;
+import com.etesync.syncadapter.HttpClient;
+import com.etesync.syncadapter.InvalidAccountException;
+import com.etesync.syncadapter.R;
+import com.etesync.syncadapter.journalmanager.Crypto;
+import com.etesync.syncadapter.journalmanager.JournalManager;
+import com.etesync.syncadapter.journalmanager.UserInfoManager;
+import com.etesync.syncadapter.model.CollectionInfo;
+
+import org.apache.commons.codec.Charsets;
+
+import lombok.RequiredArgsConstructor;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+
+public class RemoveMemberFragment extends DialogFragment {
+ final static private String KEY_MEMBER = "memberEmail";
+ private AccountSettings settings;
+ private OkHttpClient httpClient;
+ private HttpUrl remote;
+ private CollectionInfo info;
+ private String memberEmail;
+
+ public static RemoveMemberFragment newInstance(Account account, CollectionInfo info, String email) {
+ RemoveMemberFragment frag = new RemoveMemberFragment();
+ Bundle args = new Bundle(1);
+ args.putParcelable(Constants.KEY_ACCOUNT, account);
+ args.putSerializable(Constants.KEY_COLLECTION_INFO, info);
+ args.putString(KEY_MEMBER, email);
+ frag.setArguments(args);
+ return frag;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Account account = getArguments().getParcelable(Constants.KEY_ACCOUNT);
+ info = (CollectionInfo) getArguments().getSerializable(Constants.KEY_COLLECTION_INFO);
+ memberEmail = getArguments().getString(KEY_MEMBER);
+ try {
+ settings = new AccountSettings(getContext(), account);
+ httpClient = HttpClient.create(getContext(), account);
+ } catch (InvalidAccountException e) {
+ e.printStackTrace();
+ }
+ remote = HttpUrl.get(settings.getUri());
+
+ new MemberRemove().execute();
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ ProgressDialog progress = new ProgressDialog(getContext());
+ progress.setTitle(R.string.collection_members_removing);
+ progress.setMessage(getString(R.string.please_wait));
+ progress.setIndeterminate(true);
+ progress.setCanceledOnTouchOutside(false);
+ setCancelable(false);
+ return progress;
+ }
+
+ private class MemberRemove extends AsyncTask {
+ @Override
+ protected RemoveResult doInBackground(Void... voids) {
+ try {
+ JournalManager journalsManager = new JournalManager(httpClient, remote);
+ JournalManager.Journal journal = JournalManager.Journal.fakeWithUid(info.uid);
+
+ JournalManager.Member member = new JournalManager.Member(memberEmail, "placeholder".getBytes(Charsets.UTF_8));
+ journalsManager.deleteMember(journal, member);
+
+ return new RemoveResult(null);
+ } catch (Exception e) {
+ return new RemoveResult(e);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(RemoveResult result) {
+ if (result.throwable == null) {
+ ((Refreshable) getActivity()).refresh();
+ } else {
+ new AlertDialog.Builder(getActivity())
+ .setIcon(R.drawable.ic_error_dark)
+ .setTitle(R.string.collection_members_remove_error)
+ .setMessage(result.throwable.getMessage())
+ .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ }
+ }).show();
+ }
+ dismiss();
+ }
+
+ @RequiredArgsConstructor
+ class RemoveResult {
+ final Throwable throwable;
+ }
+ }
+}
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java
index a859a9ca..56ca2027 100644
--- a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java
+++ b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java
@@ -168,6 +168,24 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh
startActivity(ImportActivity.newIntent(ViewCollectionActivity.this, account, info));
}
+ public void onManageMembers(MenuItem item) {
+ if (isOwner) {
+ startActivity(CollectionMembersActivity.newIntent(this, account, info));
+ } else {
+ AlertDialog dialog = new AlertDialog.Builder(this)
+ .setIcon(R.drawable.ic_info_dark)
+ .setTitle(R.string.not_allowed_title)
+ .setMessage(getString(R.string.members_owner_only, journalEntity.getOwner()))
+ .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ }
+ }).create();
+ dialog.show();
+ }
+ }
+
private class LoadCountTask extends AsyncTask {
private int entryCount;
diff --git a/app/src/main/res/drawable/ic_account_add_dark.xml b/app/src/main/res/drawable/ic_account_add_dark.xml
new file mode 100644
index 00000000..d12a0502
--- /dev/null
+++ b/app/src/main/res/drawable/ic_account_add_dark.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_members_dark.xml b/app/src/main/res/drawable/ic_members_dark.xml
new file mode 100644
index 00000000..d5e14c6f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_members_dark.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/collection_header.xml b/app/src/main/res/layout/collection_header.xml
new file mode 100644
index 00000000..178f7fc4
--- /dev/null
+++ b/app/src/main/res/layout/collection_header.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/collection_members_list.xml b/app/src/main/res/layout/collection_members_list.xml
new file mode 100644
index 00000000..7d1950d7
--- /dev/null
+++ b/app/src/main/res/layout/collection_members_list.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/collection_members_list_item.xml b/app/src/main/res/layout/collection_members_list_item.xml
new file mode 100644
index 00000000..ab8fd805
--- /dev/null
+++ b/app/src/main/res/layout/collection_members_list_item.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_collection_activity.xml b/app/src/main/res/layout/view_collection_activity.xml
index 329badc0..652666f8 100644
--- a/app/src/main/res/layout/view_collection_activity.xml
+++ b/app/src/main/res/layout/view_collection_activity.xml
@@ -5,42 +5,11 @@
android:layout_height="match_parent"
android:orientation="vertical">
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_margin="@dimen/activity_margin" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/activity_view_collection.xml b/app/src/main/res/menu/activity_view_collection.xml
index 2a83d62a..1eb4dd3d 100644
--- a/app/src/main/res/menu/activity_view_collection.xml
+++ b/app/src/main/res/menu/activity_view_collection.xml
@@ -15,6 +15,11 @@
android:onClick="onEditCollection"
app:showAsAction="always" />
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e5a1f667..929cfd21 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -103,6 +103,20 @@
Change Journal
In order to import contacts and calendars into EteSync, you need to click on the menu, and choose \"Import\".
+ Only the owner of this collection (%s) is allowed to view its members.
+
+
+ Members
+ Loading members...
+ No members
+ Add member
+ Error adding member
+ Adding member
+ Trust fingerprint?
+ Removing member
+ Error removing member
+ Remove member
+ 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.
EteSync permissions
@@ -215,6 +229,7 @@
Description (optional):
Edit
Import
+ Manage Members
Save
Delete
Are you sure?