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?