1
0
mirror of https://github.com/etesync/android synced 2025-01-11 00:01:12 +00:00

Add UI to add/remove/list journal members.

Only owners of a journal are allowed to control and view its members.
This commit is contained in:
Tom Hacohen 2017-04-13 17:35:09 +01:00
parent 4246ae7ede
commit 656dad3615
18 changed files with 826 additions and 39 deletions

View File

@ -184,6 +184,7 @@
android:parentActivityName=".ui.AccountsActivity">
</activity>
<activity android:name=".ui.ViewCollectionActivity"/>
<activity android:name=".ui.CollectionMembersActivity"/>
<activity android:name=".ui.importlocal.ImportActivity"/>
<activity android:name=".ui.AccountSettingsActivity"/>
<activity android:name=".ui.CreateCollectionActivity"/>

View File

@ -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) {

View File

@ -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);

View File

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

View File

@ -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<Void, Void, MemberAdd.AddResult> {
@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<Void, Void, MemberAddSecond.AddResultSecond> {
@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;
}
}
}

View File

@ -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<Persistable> 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;
}
}

View File

@ -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<Persistable> 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<JournalManager.Member> {
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<Void, Void, JournalMembersFetch.MembersResult> {
@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<JournalManager.Member> members;
final Throwable throwable;
}
}
}

View File

@ -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<Void, Void, MemberRemove.RemoveResult> {
@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;
}
}
}

View File

@ -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<Void, Void, Long> {
private int entryCount;

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:alpha="0.54" >
<path
android:fillColor="#FF000000"
android:pathData="M15,14C12.33,14 7,15.33 7,18V20H23V18C23,15.33 17.67,14 15,14M6,10V7H4V10H1V12H4V15H6V12H9V10M15,12A4,4 0 0,0 19,8A4,4 0 0,0 15,4A4,4 0 0,0 11,8A4,4 0 0,0 15,12Z" />
</vector>

View File

@ -0,0 +1,5 @@
<vector android:alpha="0.54" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M16,13C15.71,13 15.38,13 15.03,13.05C16.19,13.89 17,15 17,16.5V19H23V16.5C23,14.17 18.33,13 16,13M8,13C5.67,13 1,14.17 1,16.5V19H15V16.5C15,14.17 10.33,13 8,13M8,11A3,3 0 0,0 11,8A3,3 0 0,0 8,5A3,3 0 0,0 5,8A3,3 0 0,0 8,11M16,11A3,3 0 0,0 19,8A3,3 0 0,0 16,5A3,3 0 0,0 13,8A3,3 0 0,0 16,11Z"/>
</vector>

View File

@ -0,0 +1,36 @@
<?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:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/display_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:hint="@string/create_calendar_display_name_hint"
android:textAppearance="?android:attr/textAppearanceMedium" />
<View
android:id="@+id/color"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginLeft="16dp"
android:background="@color/orangeA700" />
</LinearLayout>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
</LinearLayout>

View File

@ -0,0 +1,21 @@
<?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="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/collection_members_list_loading"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</LinearLayout>

View File

@ -0,0 +1,24 @@
<?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:padding="8dp"
android:gravity="center_vertical">
<LinearLayout android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="Title"/>
</LinearLayout>
</LinearLayout>

View File

@ -5,42 +5,11 @@
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
<include
layout="@layout/collection_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/activity_margin">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:hint="@string/create_calendar_display_name_hint"
android:textAppearance="?android:attr/textAppearanceMedium"/>
<View
android:id="@+id/color"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginLeft="16dp"
android:background="@color/orangeA700"/>
</LinearLayout>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
</LinearLayout>
android:layout_margin="@dimen/activity_margin" />
<TextView
android:layout_width="match_parent"

View File

@ -0,0 +1,61 @@
<?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="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" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="@dimen/activity_margin">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="right" />
</LinearLayout>
<LinearLayout
android:id="@+id/add_member"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/activity_margin"
android:onClick="onAddMemberClicked"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:text="@string/collection_members_add"
android:textAppearance="?android:attr/textAppearanceMedium" />
<ImageView
android:id="@+id/icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:src="@drawable/ic_account_add_dark" />
</LinearLayout>
<LinearLayout
android:id="@+id/list_entries_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
</LinearLayout>
</LinearLayout>

View File

@ -15,6 +15,11 @@
android:onClick="onEditCollection"
app:showAsAction="always" />
<item android:title="@string/view_collection_members"
android:icon="@drawable/ic_members_dark"
android:onClick="onManageMembers"
app:showAsAction="ifRoom"/>
<item android:title="@string/view_collection_import"
android:onClick="onImport"
app:showAsAction="never"/>

View File

@ -103,6 +103,20 @@
<!-- ViewCollection -->
<string name="change_journal_title">Change Journal</string>
<string name="account_showcase_import">In order to import contacts and calendars into EteSync, you need to click on the menu, and choose \"Import\".</string>
<string name="members_owner_only">Only the owner of this collection (%s) is allowed to view its members.</string>
<!-- CollectionMembers -->
<string name="collection_members_title">Members</string>
<string name="collection_members_list_loading">Loading members...</string>
<string name="collection_members_list_empty">No members</string>
<string name="collection_members_add">Add member</string>
<string name="collection_members_add_error">Error adding member</string>
<string name="collection_members_adding">Adding member</string>
<string name="trust_fingerprint_title">Trust fingerprint?</string>
<string name="collection_members_removing">Removing member</string>
<string name="collection_members_remove_error">Error removing member</string>
<string name="collection_members_remove_title">Remove member</string>
<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>
<!-- PermissionsActivity -->
<string name="permissions_title">EteSync permissions</string>
@ -215,6 +229,7 @@
<string name="create_collection_description">Description (optional):</string>
<string name="view_collection_edit">Edit</string>
<string name="view_collection_import">Import</string>
<string name="view_collection_members">Manage Members</string>
<string name="create_collection_create">Save</string>
<string name="delete_collection">Delete</string>
<string name="delete_collection_confirm_title">Are you sure?</string>