1
0
mirror of https://github.com/etesync/android synced 2024-11-22 16:08:13 +00:00

Merge: add support for sharing journals

This merge adds support for sharing journals and all the infra that
comes with it.

This means that there's now a UI to see who's the owner of a journal,
adding, removing and listing members of a journal, creating an
asymmetric keypair and storing it encrypted on the server, and viewing
and comparing pubkey fingerprints.

This is ready to be used, but not 100% complete. For example, adding
a user to a journal, waiting until the user syncs (so he has it
locally), removing his access, letting him sync again, and then adding
access back would result in the journal being visible to the user (as
expected), but the content of the journal would not be applied unless
the user removes and readds the local account.
This commit is contained in:
Tom Hacohen 2017-04-20 20:11:40 +01:00
commit 87af98f92d
46 changed files with 1870 additions and 449 deletions

View File

@ -18,7 +18,7 @@ android {
minSdkVersion 16
targetSdkVersion 25
versionCode 10
versionCode 11
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
buildConfigField "boolean", "customCerts", "true"
@ -26,7 +26,7 @@ android {
productFlavors {
standard {
versionName "0.13.0"
versionName "0.14.0"
}
}

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

@ -21,6 +21,9 @@ import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.etesync.syncadapter.journalmanager.Crypto;
import com.etesync.syncadapter.utils.Base64;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
@ -36,6 +39,8 @@ public class AccountSettings {
KEY_URI = "uri",
KEY_USERNAME = "user_name",
KEY_TOKEN = "auth_token",
KEY_ASYMMETRIC_PRIVATE_KEY = "asymmetric_private_key",
KEY_ASYMMETRIC_PUBLIC_KEY = "asymmetric_public_key",
KEY_WIFI_ONLY = "wifi_only", // sync on WiFi only (default: false)
KEY_WIFI_ONLY_SSID = "wifi_only_ssid"; // restrict sync to specific WiFi SSID
@ -122,6 +127,23 @@ public class AccountSettings {
accountManager.setUserData(account, KEY_TOKEN, token);
}
public Crypto.AsymmetricKeyPair getKeyPair() {
if (accountManager.getUserData(account, KEY_ASYMMETRIC_PUBLIC_KEY) != null) {
byte[] pubkey = Base64.decode(accountManager.getUserData(account, KEY_ASYMMETRIC_PUBLIC_KEY), Base64.NO_WRAP);
byte[] privkey = Base64.decode(accountManager.getUserData(account, KEY_ASYMMETRIC_PRIVATE_KEY), Base64.NO_WRAP);
return new Crypto.AsymmetricKeyPair(privkey, pubkey);
}
return null;
}
public void setKeyPair(@NonNull Crypto.AsymmetricKeyPair keyPair) {
accountManager.setUserData(account, KEY_ASYMMETRIC_PUBLIC_KEY, Base64.encodeToString(keyPair.getPublicKey(), Base64.NO_WRAP));
accountManager.setUserData(account, KEY_ASYMMETRIC_PRIVATE_KEY, Base64.encodeToString(keyPair.getPrivateKey(), Base64.NO_WRAP));
}
public String username() {
return accountManager.getUserData(account, KEY_USERNAME);
}

View File

@ -13,17 +13,12 @@ import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.app.Service;
import android.content.Intent;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.os.Binder;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.ServiceDB.OpenHelper;
import com.etesync.syncadapter.model.ServiceDB.Services;
import com.etesync.syncadapter.model.ServiceEntity;
import java.lang.ref.WeakReference;
import java.util.HashSet;
@ -105,31 +100,17 @@ public class AccountUpdateService extends Service {
void cleanupAccounts() {
App.log.info("Cleaning up orphaned accounts");
final OpenHelper dbHelper = new OpenHelper(this);
try {
SQLiteDatabase db = dbHelper.getWritableDatabase();
List<String> sqlAccountNames = new LinkedList<>();
AccountManager am = AccountManager.get(this);
for (Account account : am.getAccountsByType(Constants.ACCOUNT_TYPE))
sqlAccountNames.add(account.name);
List<String> sqlAccountNames = new LinkedList<>();
AccountManager am = AccountManager.get(this);
for (Account account : am.getAccountsByType(Constants.ACCOUNT_TYPE))
sqlAccountNames.add(DatabaseUtils.sqlEscapeString(account.name));
EntityDataStore<Persistable> data = ((App) getApplication()).getData();
EntityDataStore<Persistable> data = ((App) getApplication()).getData();
if (sqlAccountNames.isEmpty()) {
data.delete(JournalEntity.class).get().value();
db.delete(Services._TABLE, null, null);
} else {
Cursor cur = db.query(Services._TABLE, new String[]{Services.ID}, Services.ACCOUNT_NAME + " NOT IN (" + TextUtils.join(",", sqlAccountNames) + ")", null, null, null, null);
cur.moveToFirst();
while(!cur.isAfterLast()) {
data.delete(JournalEntity.class).where(JournalEntity.SERVICE.eq(cur.getLong(0))).get().value();
cur.moveToNext();
}
db.delete(Services._TABLE, Services.ACCOUNT_NAME + " NOT IN (" + TextUtils.join(",", sqlAccountNames) + ")", null);
}
} finally {
dbHelper.close();
if (sqlAccountNames.isEmpty()) {
data.delete(ServiceEntity.class).get().value();
} else {
data.delete(ServiceEntity.class).where(ServiceEntity.ACCOUNT.notIn(sqlAccountNames)).get().value();
}
}
}

View File

@ -42,6 +42,7 @@ import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.Models;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.model.Settings;
import com.etesync.syncadapter.resource.LocalAddressBook;
import com.etesync.syncadapter.resource.LocalCalendar;
@ -68,9 +69,9 @@ import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.ContactsStorageException;
import io.requery.Persistable;
import io.requery.android.sqlite.DatabaseSource;
import io.requery.meta.EntityModel;
import io.requery.sql.Configuration;
import io.requery.sql.EntityDataStore;
import io.requery.sql.TableCreationMode;
import lombok.Cleanup;
import lombok.Getter;
import okhttp3.internal.tls.OkHostnameVerifier;
@ -227,13 +228,43 @@ public class App extends Application {
public EntityDataStore<Persistable> getData() {
if (dataStore == null) {
// override onUpgrade to handle migrating to a new version
DatabaseSource source = new DatabaseSource(this, Models.DEFAULT, 1);
DatabaseSource source = new MyDatabaseSource(this, Models.DEFAULT, 3);
Configuration configuration = source.getConfiguration();
dataStore = new EntityDataStore<>(configuration);
}
return dataStore;
}
private static class MyDatabaseSource extends DatabaseSource {
MyDatabaseSource(Context context, EntityModel entityModel, int version) {
super(context, entityModel, version);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
super.onUpgrade(db, oldVersion, newVersion);
if (oldVersion < 3) {
db.execSQL("PRAGMA foreign_keys=OFF;");
db.execSQL("CREATE TABLE new_Journal (id integer primary key autoincrement not null, deleted boolean not null, encryptedKey varbinary(255), info varchar(255), owner varchar(255), service integer, serviceModel integer, uid varchar(64) not null, foreign key (serviceModel) references Service (id) on delete cascade);");
db.execSQL("CREATE TABLE new_Entry (id integer primary key autoincrement not null, content varchar(255), journal integer, uid varchar(64) not null, foreign key (journal) references Journal (id) on delete cascade);");
db.execSQL("INSERT INTO new_Journal SELECT id, deleted, encryptedKey, info, owner, service, serviceModel, uid from Journal;");
db.execSQL("INSERT INTO new_Entry SELECT id, content, journal, uid from Entry;");
db.execSQL("DROP TABLE Journal;");
db.execSQL("DROP TABLE Entry;");
db.execSQL("ALTER TABLE new_Journal RENAME TO Journal;");
db.execSQL("ALTER TABLE new_Entry RENAME TO Entry;");
// Add back indexes
db.execSQL("CREATE UNIQUE INDEX journal_unique_together on Journal (serviceModel, uid);");
db.execSQL("CREATE UNIQUE INDEX entry_unique_together on Entry (journal, uid);");
db.execSQL("PRAGMA foreign_keys=ON;");
}
}
}
// update from previous account settings
private final static String PREF_VERSION = "version";
@ -257,7 +288,7 @@ public class App extends Application {
List<CollectionInfo> collections = readCollections(dbHelper);
for (CollectionInfo info : collections) {
JournalEntity journalEntity = new JournalEntity(info);
JournalEntity journalEntity = new JournalEntity(data, info);
data.insert(journalEntity);
}
@ -291,6 +322,12 @@ public class App extends Application {
if (fromVersion < 10) {
HintManager.setHintSeen(this, AccountsActivity.HINT_ACCOUNT_ADD, true);
}
if (fromVersion < 11) {
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(this);
migrateServices(dbHelper);
}
}
public static class AppUpdatedReceiver extends BroadcastReceiver {
@ -321,4 +358,25 @@ public class App extends Application {
}
return collections;
}
public void migrateServices(ServiceDB.OpenHelper dbHelper) {
@Cleanup SQLiteDatabase db = dbHelper.getReadableDatabase();
EntityDataStore<Persistable> data = this.getData();
@Cleanup Cursor cursor = db.query(ServiceDB.Services._TABLE, null, null, null, null, null, null);
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
DatabaseUtils.cursorRowToContentValues(cursor, values);
ServiceEntity service = new ServiceEntity();
service.setAccount(values.getAsString(ServiceDB.Services.ACCOUNT_NAME));
service.setType(CollectionInfo.Type.valueOf(values.getAsString(ServiceDB.Services.SERVICE)));
data.insert(service);
for (JournalEntity journalEntity : data.select(JournalEntity.class).where(JournalEntity.SERVICE.eq(values.getAsLong(ServiceDB.Services.ID))).get()) {
journalEntity.setServiceModel(service);
data.update(journalEntity);
}
}
db.delete(ServiceDB.Services._TABLE, null, null);
}
}

View File

@ -74,6 +74,10 @@ public class HttpClient {
return create(context, App.log);
}
public static OkHttpClient create(@Nullable Context context, String authToken) {
return create(context, App.log, Constants.serviceUrl.getHost(), authToken);
}
private static OkHttpClient.Builder defaultBuilder(@Nullable Context context, @NonNull final Logger logger) {
OkHttpClient.Builder builder = client.newBuilder();

View File

@ -8,22 +8,13 @@
package com.etesync.syncadapter;
import android.accounts.Account;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.support.annotation.NonNull;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.ServiceDB.Services;
import com.etesync.syncadapter.resource.LocalTaskList;
import at.bitfire.ical4android.TaskProvider;
import lombok.Cleanup;
public class PackageChangedReceiver extends BroadcastReceiver {
@ -40,24 +31,7 @@ public class PackageChangedReceiver extends BroadcastReceiver {
App.log.info("Package (un)installed; OpenTasks provider now available = " + tasksInstalled);
// check all accounts and (de)activate OpenTasks if a CalDAV service is defined
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(context);
SQLiteDatabase db = dbHelper.getReadableDatabase();
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[] { Services.ACCOUNT_NAME },
Services.SERVICE + "=?", new String[] { Services.SERVICE_CALDAV }, null, null, null);
while (cursor.moveToNext()) {
Account account = new Account(cursor.getString(0), Constants.ACCOUNT_TYPE);
if (tasksInstalled) {
if (ContentResolver.getIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority) <= 0) {
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 1);
ContentResolver.setSyncAutomatically(account, TaskProvider.ProviderName.OpenTasks.authority, true);
ContentResolver.addPeriodicSync(account, TaskProvider.ProviderName.OpenTasks.authority, new Bundle(), Constants.DEFAULT_SYNC_INTERVAL);
}
} else
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0);
}
// FIXME: Do something if we ever bring back tasks.
}
}

View File

@ -7,11 +7,18 @@ import com.etesync.syncadapter.utils.Base64;
import org.apache.commons.codec.Charsets;
import org.apache.commons.lang3.ArrayUtils;
import org.spongycastle.asn1.pkcs.PrivateKeyInfo;
import org.spongycastle.asn1.x509.SubjectPublicKeyInfo;
import org.spongycastle.crypto.AsymmetricBlockCipher;
import org.spongycastle.crypto.AsymmetricCipherKeyPair;
import org.spongycastle.crypto.BufferedBlockCipher;
import org.spongycastle.crypto.CipherParameters;
import org.spongycastle.crypto.InvalidCipherTextException;
import org.spongycastle.crypto.digests.SHA256Digest;
import org.spongycastle.crypto.encodings.OAEPEncoding;
import org.spongycastle.crypto.engines.AESEngine;
import org.spongycastle.crypto.engines.RSAEngine;
import org.spongycastle.crypto.generators.RSAKeyPairGenerator;
import org.spongycastle.crypto.generators.SCrypt;
import org.spongycastle.crypto.macs.HMac;
import org.spongycastle.crypto.modes.CBCBlockCipher;
@ -20,12 +27,21 @@ import org.spongycastle.crypto.paddings.PKCS7Padding;
import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.crypto.params.ParametersWithIV;
import org.spongycastle.crypto.params.RSAKeyGenerationParameters;
import org.spongycastle.crypto.util.PrivateKeyFactory;
import org.spongycastle.crypto.util.PrivateKeyInfoFactory;
import org.spongycastle.crypto.util.PublicKeyFactory;
import org.spongycastle.crypto.util.SubjectPublicKeyInfoFactory;
import org.spongycastle.util.encoders.Hex;
import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Arrays;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
public class Crypto {
public static String deriveKey(String salt, String password) {
@ -34,17 +50,103 @@ public class Crypto {
return Base64.encodeToString(SCrypt.generate(password.getBytes(Charsets.UTF_8), salt.getBytes(Charsets.UTF_8), 16384, 8, 1, keySize), Base64.NO_WRAP);
}
public static AsymmetricKeyPair generateKeyPair() {
RSAKeyPairGenerator keyPairGenerator = new RSAKeyPairGenerator();
keyPairGenerator.init(new RSAKeyGenerationParameters(BigInteger.valueOf(65537), new SecureRandom(), 2048, 160));
AsymmetricCipherKeyPair keyPair = keyPairGenerator.generateKeyPair();
try {
PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.createPrivateKeyInfo(keyPair.getPrivate());
SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(keyPair.getPublic());
return new AsymmetricKeyPair(privateKeyInfo.getEncoded(), publicKeyInfo.getEncoded());
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@RequiredArgsConstructor
public static class AsymmetricKeyPair {
@Getter(AccessLevel.PUBLIC)
private final byte[] privateKey;
@Getter(AccessLevel.PUBLIC)
private final byte[] publicKey;
}
public static class AsymmetricCryptoManager {
private final AsymmetricKeyPair keyPair;
public AsymmetricCryptoManager(AsymmetricKeyPair keyPair) {
this.keyPair = keyPair;
}
public byte[] encrypt(byte[] pubkey, byte[] content) {
AsymmetricBlockCipher cipher = new RSAEngine();
cipher = new OAEPEncoding(cipher);
try {
cipher.init(true, PublicKeyFactory.createKey(pubkey));
return cipher.processBlock(content, 0, content.length);
} catch (IOException e) {
e.printStackTrace();
} catch (InvalidCipherTextException e) {
e.printStackTrace();
App.log.severe("Invalid ciphertext: " + Base64.encodeToString(content, Base64.NO_WRAP));
}
return null;
}
public byte[] decrypt(byte[] cipherText) {
AsymmetricBlockCipher cipher = new RSAEngine();
cipher = new OAEPEncoding(cipher);
try {
cipher.init(false, PrivateKeyFactory.createKey(keyPair.getPrivateKey()));
return cipher.processBlock(cipherText, 0, cipherText.length);
} catch (IOException e) {
e.printStackTrace();
} catch (InvalidCipherTextException e) {
e.printStackTrace();
App.log.severe("Invalid ciphertext: " + Base64.encodeToString(cipherText, Base64.NO_WRAP));
}
return null;
}
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 {
final static int HMAC_SIZE = 256 / 8; // hmac256 in bytes
private SecureRandom _random = null;
@Getter
private final byte version;
private final byte[] cipherKey;
private final byte[] hmacKey;
private byte[] cipherKey;
private byte[] hmacKey;
private byte[] derivedKey;
private void setDerivedKey(byte[] derivedKey) {
cipherKey = hmac256("aes".getBytes(Charsets.UTF_8), derivedKey);
hmacKey = hmac256("hmac".getBytes(Charsets.UTF_8), derivedKey);
}
public CryptoManager(int version, AsymmetricKeyPair keyPair, byte[] encryptedKey) {
Crypto.AsymmetricCryptoManager cryptoManager = new Crypto.AsymmetricCryptoManager(keyPair);
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) {
@ -56,8 +158,7 @@ public class Crypto {
}
this.version = (byte) version;
cipherKey = hmac256("aes".getBytes(Charsets.UTF_8), derivedKey);
hmacKey = hmac256("hmac".getBytes(Charsets.UTF_8), derivedKey);
setDerivedKey(derivedKey);
}
private static final int blockSize = 16; // AES's block size in bytes
@ -143,18 +244,23 @@ 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) {
return sha256(base.getBytes(Charsets.UTF_8));
return toHex(sha256(base.getBytes(Charsets.UTF_8)));
}
private static String sha256(byte[] base) {
private static byte[] sha256(byte[] base) {
SHA256Digest digest = new SHA256Digest();
digest.update(base, 0, base.length);
byte[] ret = new byte[digest.getDigestSize()];
digest.doFinal(ret, 0);
return toHex(ret);
return ret;
}
static String toHex(byte[] bytes) {

View File

@ -1,16 +1,16 @@
package com.etesync.syncadapter.journalmanager;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.GsonHelper;
import com.google.gson.reflect.TypeToken;
import org.spongycastle.util.Arrays;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.UUID;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.GsonHelper;
import lombok.Getter;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
@ -26,6 +26,8 @@ import static com.etesync.syncadapter.journalmanager.Crypto.toHex;
public class JournalManager extends BaseManager {
final static private Type journalType = new TypeToken<List<Journal>>() {
}.getType();
final static private Type memberType = new TypeToken<List<Member>>() {
}.getType();
public JournalManager(OkHttpClient httpClient, HttpUrl remote) {
@ -38,7 +40,7 @@ public class JournalManager extends BaseManager {
this.client = httpClient;
}
public List<Journal> getJournals(String keyBase64) throws Exceptions.HttpException, Exceptions.IntegrityException, Exceptions.GenericCryptoException {
public List<Journal> getJournals() throws Exceptions.HttpException {
Request request = new Request.Builder()
.get()
.url(remote)
@ -49,9 +51,7 @@ public class JournalManager extends BaseManager {
List<Journal> ret = GsonHelper.gson.fromJson(body.charStream(), journalType);
for (Journal journal : ret) {
Crypto.CryptoManager crypto = new Crypto.CryptoManager(journal.getVersion(), keyBase64, journal.getUid());
journal.processFromJson();
journal.verify(crypto);
}
return ret;
@ -90,7 +90,50 @@ public class JournalManager extends BaseManager {
newCall(request);
}
private HttpUrl getMemberRemote(Journal journal) {
return this.remote.resolve(journal.getUid() + "/members/");
}
public List<Member> listMembers(Journal journal) throws Exceptions.HttpException, Exceptions.IntegrityException, Exceptions.GenericCryptoException {
Request request = new Request.Builder()
.get()
.url(getMemberRemote(journal))
.build();
Response response = newCall(request);
ResponseBody body = response.body();
return GsonHelper.gson.fromJson(body.charStream(), memberType);
}
public void deleteMember(Journal journal, Member member) throws Exceptions.HttpException {
RequestBody body = RequestBody.create(JSON, member.toJson());
Request request = new Request.Builder()
.delete(body)
.url(getMemberRemote(journal))
.build();
newCall(request);
}
public void addMember(Journal journal, Member member) throws Exceptions.HttpException {
RequestBody body = RequestBody.create(JSON, member.toJson());
Request request = new Request.Builder()
.post(body)
.url(getMemberRemote(journal))
.build();
newCall(request);
}
public static class Journal extends Base {
@Getter
private String owner;
@Getter
private byte[] key;
@Getter
private int version = -1;
@ -101,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);
@ -112,7 +161,7 @@ public class JournalManager extends BaseManager {
setContent(Arrays.copyOfRange(getContent(), HMAC_SIZE, getContent().length));
}
void verify(Crypto.CryptoManager crypto) throws Exceptions.IntegrityException {
public void verify(Crypto.CryptoManager crypto) throws Exceptions.IntegrityException {
if (hmac == null) {
throw new Exceptions.IntegrityException("HMAC is null!");
}
@ -140,4 +189,24 @@ public class JournalManager extends BaseManager {
return ret;
}
}
public static class Member {
@Getter
private String user;
@Getter
private byte[] key;
@SuppressWarnings("unused")
private Member() {
}
public Member(String user, byte[] encryptedKey) {
this.user = user;
this.key = encryptedKey;
}
String toJson() {
return GsonHelper.gson.toJson(this, getClass());
}
}
}

View File

@ -0,0 +1,149 @@
package com.etesync.syncadapter.journalmanager;
import com.etesync.syncadapter.GsonHelper;
import org.apache.commons.codec.Charsets;
import org.spongycastle.util.Arrays;
import java.io.IOException;
import java.net.HttpURLConnection;
import lombok.Getter;
import lombok.Setter;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import static com.etesync.syncadapter.journalmanager.Crypto.CryptoManager.HMAC_SIZE;
import static com.etesync.syncadapter.journalmanager.Crypto.toHex;
public class UserInfoManager extends BaseManager {
public UserInfoManager(OkHttpClient httpClient, HttpUrl remote) {
this.remote = remote.newBuilder()
.addPathSegments("api/v1/user")
.addPathSegment("")
.build();
this.client = httpClient;
}
public UserInfo get(String owner) throws Exceptions.HttpException {
HttpUrl remote = this.remote.newBuilder().addPathSegment(owner).addPathSegment("").build();
Request request = new Request.Builder()
.get()
.url(remote)
.build();
Response response;
try {
response = newCall(request);
} catch (Exceptions.HttpException e) {
if (e.status == HttpURLConnection.HTTP_NOT_FOUND) {
return null;
} else {
throw e;
}
}
ResponseBody body = response.body();
UserInfo ret = GsonHelper.gson.fromJson(body.charStream(), UserInfo.class);
ret.setOwner(owner);
return ret;
}
public void delete(UserInfo userInfo) throws Exceptions.HttpException {
HttpUrl remote = this.remote.newBuilder().addPathSegment(userInfo.getOwner()).addPathSegment("").build();
Request request = new Request.Builder()
.delete()
.url(remote)
.build();
newCall(request);
}
public void create(UserInfo userInfo) throws Exceptions.HttpException {
RequestBody body = RequestBody.create(JSON, userInfo.toJson());
Request request = new Request.Builder()
.post(body)
.url(remote)
.build();
newCall(request);
}
public void update(UserInfo userInfo) throws Exceptions.HttpException {
HttpUrl remote = this.remote.newBuilder().addPathSegment(userInfo.getOwner()).addPathSegment("").build();
RequestBody body = RequestBody.create(JSON, userInfo.toJson());
Request request = new Request.Builder()
.put(body)
.url(remote)
.build();
newCall(request);
}
public static class UserInfo {
@Setter
@Getter
private transient String owner;
@Getter
private byte version;
@Getter
private byte[] pubkey;
private byte[] content;
public byte[] getContent(Crypto.CryptoManager crypto) {
byte[] content = Arrays.copyOfRange(this.content, HMAC_SIZE, this.content.length);
return crypto.decrypt(content);
}
void setContent(Crypto.CryptoManager crypto, byte[] rawContent) {
byte[] content = crypto.encrypt(rawContent);
this.content = Arrays.concatenate(calculateHmac(crypto, content), content);
}
public void verify(Crypto.CryptoManager crypto) throws Exceptions.IntegrityException {
if (this.content == null) {
// Nothing to verify.
return;
}
byte[] hmac = Arrays.copyOfRange(this.content, 0, HMAC_SIZE);
byte[] content = Arrays.copyOfRange(this.content, HMAC_SIZE, this.content.length);
byte[] correctHash = calculateHmac(crypto, content);
if (!Arrays.areEqual(hmac, correctHash)) {
throw new Exceptions.IntegrityException("Bad HMAC. " + toHex(hmac) + " != " + toHex(correctHash));
}
}
private byte[] calculateHmac(Crypto.CryptoManager crypto, byte[] content) {
return crypto.hmac(Arrays.concatenate(content, pubkey));
}
private UserInfo() {
}
public UserInfo(Crypto.CryptoManager crypto, String owner, byte[] pubkey, byte[] content) {
this.owner = owner;
this.pubkey = pubkey;
version = crypto.getVersion();
setContent(crypto, content);
}
public static UserInfo generate(Crypto.CryptoManager cryptoManager, String owner) throws IOException {
Crypto.AsymmetricKeyPair keyPair = Crypto.generateKeyPair();
return new UserInfo(cryptoManager, owner, keyPair.getPublicKey(), keyPair.getPrivateKey());
}
String toJson() {
return GsonHelper.gson.toJson(this, getClass());
}
}
}

View File

@ -18,6 +18,8 @@ import com.google.gson.annotations.Expose;
import java.io.Serializable;
import io.requery.Persistable;
import io.requery.sql.EntityDataStore;
import lombok.ToString;
@ToString(exclude = {"id"})
@ -25,7 +27,7 @@ public class CollectionInfo implements Serializable {
@Deprecated
public long id;
public Long serviceID;
public int serviceID;
public enum Type {
ADDRESS_BOOK,
@ -80,7 +82,7 @@ public class CollectionInfo implements Serializable {
public static CollectionInfo fromDB(ContentValues values) {
CollectionInfo info = new CollectionInfo();
info.id = values.getAsLong(Collections.ID);
info.serviceID = values.getAsLong(Collections.SERVICE_ID);
info.serviceID = values.getAsInteger(Collections.SERVICE_ID);
info.uid = values.getAsString(Collections.URL);
info.readOnly = values.getAsInteger(Collections.READ_ONLY) != 0;
@ -95,6 +97,10 @@ public class CollectionInfo implements Serializable {
return info;
}
public ServiceEntity getServiceEntity(EntityDataStore<Persistable> data) {
return data.findByKey(ServiceEntity.class, serviceID);
}
public static CollectionInfo fromJson(String json) {
return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, CollectionInfo.class);
}

View File

@ -15,28 +15,41 @@ import io.requery.ManyToOne;
import io.requery.Persistable;
import io.requery.PostLoad;
import io.requery.ReferentialAction;
import io.requery.Table;
import io.requery.sql.EntityDataStore;
public class JournalModel {
// FIXME: Add unique constraint on the uid + service combination. Can't do it at the moment because requery is broken.
@Entity
@Table(name = "Journal")
public static abstract class Journal {
@Key
@Generated
int id;
@Column(length = 64, unique = true, nullable = false)
@Column(length = 64, nullable = false)
String uid;
@Convert(CollectionInfoConverter.class)
CollectionInfo info;
String owner;
byte[] encryptedKey;
@Deprecated
long service;
@ForeignKey(update = ReferentialAction.CASCADE)
@ManyToOne
Service serviceModel;
boolean deleted;
@PostLoad
void afterLoad() {
this.info.serviceID = service;
this.info.serviceID = this.serviceModel.id;
this.info.uid = uid;
}
@ -44,17 +57,22 @@ public class JournalModel {
this.deleted = false;
}
public Journal(CollectionInfo info) {
public Journal(EntityDataStore<Persistable> data, CollectionInfo info) {
this();
this.info = info;
this.uid = info.uid;
this.service = info.serviceID;
this.serviceModel = info.getServiceEntity(data);
}
public static List<CollectionInfo> getCollections(EntityDataStore<Persistable> data, long service) {
public static List<JournalEntity> getJournals(EntityDataStore<Persistable> data, ServiceEntity serviceEntity) {
return data.select(JournalEntity.class).where(JournalEntity.SERVICE_MODEL.eq(serviceEntity).and(JournalEntity.DELETED.eq(false))).get().toList();
}
public static List<CollectionInfo> getCollections(EntityDataStore<Persistable> data, ServiceEntity serviceEntity) {
List<CollectionInfo> ret = new LinkedList<>();
for (JournalEntity journal : data.select(JournalEntity.class).where(JournalEntity.SERVICE.eq(service).and(JournalEntity.DELETED.eq(false))).get()) {
List<JournalEntity> journals = getJournals(data, serviceEntity);
for (JournalEntity journal : journals) {
// FIXME: For some reason this isn't always being called, manually do it here.
journal.afterLoad();
ret.add(journal.getInfo());
@ -63,8 +81,8 @@ public class JournalModel {
return ret;
}
public static JournalEntity fetch(EntityDataStore<Persistable> data, String url) {
JournalEntity ret = data.select(JournalEntity.class).where(JournalEntity.UID.eq(url)).limit(1).get().firstOrNull();
public static JournalEntity fetch(EntityDataStore<Persistable> data, ServiceEntity serviceEntity, String uid) {
JournalEntity ret = data.select(JournalEntity.class).where(JournalEntity.SERVICE_MODEL.eq(serviceEntity).and(JournalEntity.UID.eq(uid))).limit(1).get().firstOrNull();
if (ret != null) {
// FIXME: For some reason this isn't always being called, manually do it here.
ret.afterLoad();
@ -73,9 +91,9 @@ public class JournalModel {
}
public static JournalEntity fetchOrCreate(EntityDataStore<Persistable> data, CollectionInfo collection) {
JournalEntity journalEntity = fetch(data, collection.uid);
JournalEntity journalEntity = fetch(data, collection.getServiceEntity(data), collection.uid);
if (journalEntity == null) {
journalEntity = new JournalEntity(collection);
journalEntity = new JournalEntity(data, collection);
} else {
journalEntity.setInfo(collection);
}
@ -93,23 +111,45 @@ public class JournalModel {
}
@Entity
@Table(name = "Entry", uniqueIndexes = "entry_unique_together")
public static abstract class Entry {
@Key
@Generated
int id;
@Column(length = 64, unique = true, nullable = false)
@Index("entry_unique_together")
@Column(length = 64, nullable = false)
String uid;
@Convert(SyncEntryConverter.class)
SyncEntry content;
@Index("journal_index")
@Index("entry_unique_together")
@ForeignKey(update = ReferentialAction.CASCADE)
@ManyToOne
Journal journal;
}
@Entity
@Table(name = "Service", uniqueIndexes = "service_unique_together")
public static abstract class Service {
@Key
@Generated
int id;
@Index(value = "service_unique_together")
@Column(nullable = false)
String account;
@Index(value = "service_unique_together")
CollectionInfo.Type type;
public static ServiceEntity fetch(EntityDataStore<Persistable> data, String account, CollectionInfo.Type type) {
return data.select(ServiceEntity.class).where(ServiceEntity.ACCOUNT.eq(account).and(ServiceEntity.TYPE.eq(type))).limit(1).get().firstOrNull();
}
}
static class CollectionInfoConverter implements Converter<CollectionInfo, String> {
@Override
public Class<CollectionInfo> getMappedType() {

View File

@ -33,17 +33,13 @@ public class ServiceDB {
VALUE = "value";
}
@Deprecated
public static class Services {
public static final String
_TABLE = "services",
ID = "_id",
ACCOUNT_NAME = "accountName",
SERVICE = "service";
// allowed values for SERVICE column
public static final String
SERVICE_CALDAV = CollectionInfo.Type.CALENDAR.toString(),
SERVICE_CARDDAV = CollectionInfo.Type.ADDRESS_BOOK.toString();
}
@Deprecated
@ -90,15 +86,8 @@ public class ServiceDB {
db.execSQL("CREATE TABLE " + Settings._TABLE + "(" +
Settings.NAME + " TEXT NOT NULL," +
Settings.VALUE + " TEXT NOT NULL" +
")");
")");
db.execSQL("CREATE UNIQUE INDEX settings_name ON " + Settings._TABLE + " (" + Settings.NAME + ")");
db.execSQL("CREATE TABLE " + Services._TABLE + "(" +
Services.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Services.ACCOUNT_NAME + " TEXT NOT NULL," +
Services.SERVICE + " TEXT NOT NULL" +
")");
db.execSQL("CREATE UNIQUE INDEX services_account ON " + Services._TABLE + " (" + Services.ACCOUNT_NAME + "," + Services.SERVICE + ")");
}
@Override
@ -112,7 +101,7 @@ public class ServiceDB {
db.beginTransactionNonExclusive();
// iterate through all tables
@Cleanup Cursor cursorTables = db.query("sqlite_master", new String[] { "name" }, "type='table'", null, null, null, null);
@Cleanup Cursor cursorTables = db.query("sqlite_master", new String[]{"name"}, "type='table'", null, null, null, null);
while (cursorTables.moveToNext()) {
String table = cursorTables.getString(0);
sb.append(table).append("\n");
@ -153,47 +142,5 @@ public class ServiceDB {
}
db.endTransaction();
}
@NonNull
public Account getServiceAccount(SQLiteDatabase db, long service) {
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[]{Services.ACCOUNT_NAME}, Services.ID + "=?", new String[]{String.valueOf(service)}, null, null, null);
if (cursor.moveToNext()) {
return new Account(cursor.getString(0), Constants.ACCOUNT_TYPE);
} else
throw new IllegalArgumentException("Service not found");
}
@NonNull
public String getServiceType(SQLiteDatabase db, long service) {
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[]{Services.SERVICE}, Services.ID + "=?", new String[]{String.valueOf(service)}, null, null, null);
if (cursor.moveToNext())
return cursor.getString(0);
else
throw new IllegalArgumentException("Service not found");
}
@Nullable
public Long getService(@NonNull SQLiteDatabase db, @NonNull Account account, String service) {
@Cleanup Cursor c = db.query(Services._TABLE, new String[]{Services.ID},
Services.ACCOUNT_NAME + "=? AND " + Services.SERVICE + "=?", new String[]{account.name, service}, null, null, null);
if (c.moveToNext())
return c.getLong(0);
else
return null;
}
@Nullable
public Long getService(@NonNull Account account, String service) {
@Cleanup SQLiteDatabase db = getReadableDatabase();
return getService(db, account, service);
}
}
public static void onRenameAccount(@NonNull SQLiteDatabase db, @NonNull String oldName, @NonNull String newName) {
ContentValues values = new ContentValues(1);
values.put(Services.ACCOUNT_NAME, newName);
db.update(Services._TABLE, values, Services.ACCOUNT_NAME + "=?", new String[] { oldName });
}
}

View File

@ -14,7 +14,6 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.Bundle;
import android.provider.CalendarContract;
@ -27,8 +26,8 @@ import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.ServiceDB.Services;
import com.etesync.syncadapter.model.JournalModel;
import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.resource.LocalCalendar;
import com.etesync.syncadapter.ui.DebugInfoActivity;
@ -109,47 +108,40 @@ public class CalendarsSyncAdapterService extends SyncAdapterService {
}
private void updateLocalCalendars(ContentProviderClient provider, Account account, AccountSettings settings) throws CalendarStorageException {
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
try {
// enumerate remote and local calendars
SQLiteDatabase db = dbHelper.getReadableDatabase();
Long service = dbHelper.getService(db, account, Services.SERVICE_CALDAV);
EntityDataStore<Persistable> data = ((App) getContext().getApplicationContext()).getData();
ServiceEntity service = JournalModel.Service.fetch(data, account.name, CollectionInfo.Type.CALENDAR);
EntityDataStore<Persistable> data = ((App) getContext().getApplicationContext()).getData();
Map<String, CollectionInfo> remote = new HashMap<>();
List<CollectionInfo> remoteCollections = JournalEntity.getCollections(data, service);
for (CollectionInfo info : remoteCollections) {
remote.put(info.uid, info);
}
Map<String, CollectionInfo> remote = new HashMap<>();
List<CollectionInfo> remoteCollections = JournalEntity.getCollections(data, service);
for (CollectionInfo info : remoteCollections) {
remote.put(info.uid, info);
}
LocalCalendar[] local = (LocalCalendar[])LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, null, null);
LocalCalendar[] local = (LocalCalendar[]) LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, null, null);
boolean updateColors = settings.getManageCalendarColors();
boolean updateColors = settings.getManageCalendarColors();
// delete obsolete local calendar
for (LocalCalendar calendar : local) {
String url = calendar.getName();
if (!remote.containsKey(url)) {
App.log.fine("Deleting obsolete local calendar " + url);
calendar.delete();
} else {
// remote CollectionInfo found for this local collection, update data
CollectionInfo info = remote.get(url);
App.log.fine("Updating local calendar " + url + " with " + info);
calendar.update(info, updateColors);
// we already have a local calendar for this remote collection, don't take into consideration anymore
remote.remove(url);
}
}
// create new local calendars
for (String url : remote.keySet()) {
// delete obsolete local calendar
for (LocalCalendar calendar : local) {
String url = calendar.getName();
if (!remote.containsKey(url)) {
App.log.fine("Deleting obsolete local calendar " + url);
calendar.delete();
} else {
// remote CollectionInfo found for this local collection, update data
CollectionInfo info = remote.get(url);
App.log.info("Adding local calendar list " + info);
LocalCalendar.create(account, provider, info);
App.log.fine("Updating local calendar " + url + " with " + info);
calendar.update(info, updateColors);
// we already have a local calendar for this remote collection, don't take into consideration anymore
remote.remove(url);
}
} finally {
dbHelper.close();
}
// create new local calendars
for (String url : remote.keySet()) {
CollectionInfo info = remote.get(url);
App.log.info("Adding local calendar list " + info);
LocalCalendar.create(account, provider, info);
}
}
}

View File

@ -14,7 +14,6 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import com.etesync.syncadapter.AccountSettings;
@ -26,7 +25,9 @@ import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.JournalModel;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.ui.DebugInfoActivity;
import java.util.logging.Level;
@ -57,7 +58,6 @@ public class ContactsSyncAdapterService extends SyncAdapterService {
NotificationHelper notificationManager = new NotificationHelper(getContext(), "journals-contacts", Constants.NOTIFICATION_CONTACTS_SYNC);
notificationManager.cancel();
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
try {
AccountSettings settings = new AccountSettings(getContext(), account);
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
@ -65,11 +65,12 @@ public class ContactsSyncAdapterService extends SyncAdapterService {
new RefreshCollections(account, CollectionInfo.Type.ADDRESS_BOOK).run();
SQLiteDatabase db = dbHelper.getReadableDatabase();
Long service = dbHelper.getService(db, account, ServiceDB.Services.SERVICE_CARDDAV);
EntityDataStore<Persistable> data = ((App) getContext().getApplicationContext()).getData();
ServiceEntity service = JournalModel.Service.fetch(data, account.name, CollectionInfo.Type.ADDRESS_BOOK);
if (service != null) {
HttpUrl principal = HttpUrl.get(settings.getUri());
EntityDataStore<Persistable> data = ((App) getContext().getApplicationContext()).getData();
CollectionInfo info = JournalEntity.getCollections(data, service).get(0);
try {
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, settings, extras, authority, provider, syncResult, principal, info);
@ -95,8 +96,6 @@ public class ContactsSyncAdapterService extends SyncAdapterService {
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase);
}
notificationManager.notify(title, getContext().getString(syncPhase));
} finally {
dbHelper.close();
}
App.log.info("Address book sync complete");

View File

@ -28,6 +28,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.util.Pair;
import java.util.HashMap;
import java.util.LinkedList;
@ -46,7 +47,9 @@ import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.journalmanager.JournalManager;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.JournalModel;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.ui.PermissionsActivity;
import io.requery.Persistable;
@ -130,7 +133,6 @@ public abstract class SyncAdapterService extends Service {
}
protected class RefreshCollections {
final private ServiceDB.OpenHelper dbHelper;
final private Account account;
final private Context context;
final private CollectionInfo.Type serviceType;
@ -139,77 +141,75 @@ public abstract class SyncAdapterService extends Service {
this.account = account;
this.serviceType = serviceType;
context = getContext();
dbHelper = new ServiceDB.OpenHelper(context);
}
void run() throws Exceptions.HttpException, Exceptions.IntegrityException, InvalidAccountException, Exceptions.GenericCryptoException {
try {
@Cleanup SQLiteDatabase db = dbHelper.getWritableDatabase();
App.log.info("Refreshing " + serviceType + " collections of service #" + serviceType.toString());
App.log.info("Refreshing " + serviceType + " collections of service #" + serviceType.toString());
OkHttpClient httpClient = HttpClient.create(context, account);
OkHttpClient httpClient = HttpClient.create(context, account);
AccountSettings settings = new AccountSettings(context, account);
JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri()));
AccountSettings settings = new AccountSettings(context, account);
JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri()));
List<Pair<JournalManager.Journal, CollectionInfo>> journals = new LinkedList<>();
List<CollectionInfo> collections = new LinkedList<>();
for (JournalManager.Journal journal : journalsManager.getJournals(settings.password())) {
Crypto.CryptoManager crypto = new Crypto.CryptoManager(journal.getVersion(), settings.password(), journal.getUid());
CollectionInfo info = CollectionInfo.fromJson(journal.getContent(crypto));
info.updateFromJournal(journal);
if (info.type.equals(serviceType)) {
collections.add(info);
}
for (JournalManager.Journal journal : journalsManager.getJournals()) {
Crypto.CryptoManager crypto;
if (journal.getKey() != null) {
crypto = new Crypto.CryptoManager(journal.getVersion(), settings.getKeyPair(), journal.getKey());
} else {
crypto = new Crypto.CryptoManager(journal.getVersion(), settings.password(), journal.getUid());
}
if (collections.isEmpty()) {
CollectionInfo info = CollectionInfo.defaultForServiceType(serviceType);
info.uid = JournalManager.Journal.genUid();
Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid);
JournalManager.Journal journal = new JournalManager.Journal(crypto, info.toJson(), info.uid);
journalsManager.putJournal(journal);
collections.add(info);
}
journal.verify(crypto);
db.beginTransactionNonExclusive();
try {
saveCollections(db, collections);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
CollectionInfo info = CollectionInfo.fromJson(journal.getContent(crypto));
info.updateFromJournal(journal);
if (info.type.equals(serviceType)) {
journals.add(new Pair<>(journal, info));
}
} finally {
dbHelper.close();
}
if (journals.isEmpty()) {
CollectionInfo info = CollectionInfo.defaultForServiceType(serviceType);
info.uid = JournalManager.Journal.genUid();
Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid);
JournalManager.Journal journal = new JournalManager.Journal(crypto, info.toJson(), info.uid);
journalsManager.putJournal(journal);
journals.add(new Pair<>(journal, info));
}
saveCollections(journals);
}
private void saveCollections(SQLiteDatabase db, Iterable<CollectionInfo> collections) {
Long service = dbHelper.getService(db, account, serviceType.toString());
private void saveCollections(Iterable<Pair<JournalManager.Journal, CollectionInfo>> journals) {
EntityDataStore<Persistable> data = ((App) context.getApplicationContext()).getData();
Map<String, CollectionInfo> existing = new HashMap<>();
List<CollectionInfo> existingList = JournalEntity.getCollections(data, service);
for (CollectionInfo info : existingList) {
existing.put(info.uid, info);
ServiceEntity service = JournalModel.Service.fetch(data, account.name, serviceType);
Map<String, JournalEntity> existing = new HashMap<>();
for (JournalEntity journalEntity : JournalEntity.getJournals(data, service)) {
existing.put(journalEntity.getUid(), journalEntity);
}
for (CollectionInfo collection : collections) {
App.log.log(Level.FINE, "Saving collection", collection.uid);
for (Pair<JournalManager.Journal, CollectionInfo> pair : journals) {
JournalManager.Journal journal = pair.first;
CollectionInfo collection = pair.second;
App.log.log(Level.FINE, "Saving collection", journal.getUid());
collection.serviceID = service;
collection.serviceID = service.getId();
JournalEntity journalEntity = JournalEntity.fetchOrCreate(data, collection);
journalEntity.setOwner(journal.getOwner());
journalEntity.setEncryptedKey(journal.getKey());
journalEntity.setDeleted(false);
data.upsert(journalEntity);
existing.remove(collection.uid);
}
for (CollectionInfo collection : existing.values()) {
App.log.log(Level.FINE, "Deleting collection", collection.uid);
for (JournalEntity journalEntity : existing.values()) {
App.log.log(Level.FINE, "Deleting collection", journalEntity.getUid());
JournalEntity journalEntity = data.select(JournalEntity.class).where(JournalEntity.UID.eq(collection.uid)).limit(1).get().first();
journalEntity.setDeleted(true);
data.update(journalEntity);
}

View File

@ -27,10 +27,13 @@ import com.etesync.syncadapter.journalmanager.JournalEntryManager;
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 com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.model.SyncEntry;
import com.etesync.syncadapter.resource.LocalCollection;
import com.etesync.syncadapter.resource.LocalResource;
import com.etesync.syncadapter.ui.DebugInfoActivity;
import com.etesync.syncadapter.utils.Base64;
import org.apache.commons.collections4.ListUtils;
@ -108,14 +111,20 @@ abstract public class SyncManager {
httpClient = HttpClient.create(context, account);
data = ((App) context.getApplicationContext()).getData();
info = JournalEntity.fetch(data, journalUid).getInfo();
ServiceEntity serviceEntity = JournalModel.Service.fetch(data, account.name, serviceType);
info = JournalEntity.fetch(data, serviceEntity, journalUid).getInfo();
// dismiss previous error notifications
notificationManager = new NotificationHelper(context, journalUid, notificationId());
notificationManager.cancel();
App.log.info(String.format(Locale.getDefault(), "Syncing collection %s (version: %d)", journalUid, info.version));
crypto = new Crypto.CryptoManager(info.version, settings.password(), journalUid);
if (getJournalEntity().getEncryptedKey() != null) {
crypto = new Crypto.CryptoManager(info.version, settings.getKeyPair(), getJournalEntity().getEncryptedKey());
} else {
crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid);
}
}
protected abstract int notificationId();
@ -232,7 +241,7 @@ abstract public class SyncManager {
private JournalEntity getJournalEntity() {
if (_journalEntity == null)
_journalEntity = data.select(JournalEntity.class).where(JournalEntity.UID.eq(journal.getUid())).limit(1).get().first();
_journalEntity = JournalModel.Journal.fetch(data, info.getServiceEntity(data), info.uid);
return _journalEntity;
}

View File

@ -24,8 +24,6 @@ import android.content.Intent;
import android.content.Loader;
import android.content.ServiceConnection;
import android.content.SyncStatusObserver;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
@ -51,15 +49,17 @@ import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.AccountUpdateService;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.Constants;
import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Crypto;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.ServiceDB.OpenHelper;
import com.etesync.syncadapter.model.ServiceDB.Services;
import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.resource.LocalCalendar;
import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment;
import com.etesync.syncadapter.utils.HintManager;
import com.etesync.syncadapter.utils.ShowcaseBuilder;
@ -71,7 +71,6 @@ import at.bitfire.cert4android.CustomCertManager;
import at.bitfire.ical4android.TaskProvider;
import io.requery.Persistable;
import io.requery.sql.EntityDataStore;
import lombok.Cleanup;
import tourguide.tourguide.ToolTip;
import static android.content.ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE;
@ -108,7 +107,7 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
tbCalDAV.setOnMenuItemClickListener(this);
tbCalDAV.setTitle(R.string.settings_caldav);
// load CardDAV/CalDAV collections
// load CardDAV/CalDAV journals
getLoaderManager().initLoader(0, getIntent().getExtras(), this);
if (!HintManager.getHintSeen(this, HINT_VIEW_COLLECTION)) {
@ -117,6 +116,10 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
.playOn(tbCardDAV);
HintManager.setHintSeen(this, HINT_VIEW_COLLECTION, true);
}
if (!SetupUserInfoFragment.hasUserInfo(this, account)) {
SetupUserInfoFragment.newInstance(account).show(getSupportFragmentManager(), null);
}
}
@Override
@ -166,6 +169,19 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
})
.show();
break;
case R.id.show_fingerprint:
AlertDialog dialog = new AlertDialog.Builder(AccountActivity.this)
.setIcon(R.drawable.ic_fingerprint_dark)
.setTitle(R.string.show_fingperprint_title)
.setMessage(getFormattedFingerprint())
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
}).create();
dialog.show();
break;
default:
return super.onOptionsItemSelected(item);
}
@ -188,13 +204,25 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final ListView list = (ListView)parent;
final ArrayAdapter<CollectionInfo> adapter = (ArrayAdapter)list.getAdapter();
final CollectionInfo info = adapter.getItem(position);
final ArrayAdapter<JournalEntity> adapter = (ArrayAdapter)list.getAdapter();
final JournalEntity journalEntity = adapter.getItem(position);
final CollectionInfo info = journalEntity.getInfo();
startActivity(ViewCollectionActivity.newIntent(AccountActivity.this, account, info));
}
};
private String getFormattedFingerprint() {
AccountSettings settings = null;
try {
settings = new AccountSettings(this, account);
return Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(settings.getKeyPair().getPublicKey());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/* LOADERS AND LOADED DATA */
protected static class AccountInfo {
@ -204,7 +232,7 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
long id;
boolean refreshing;
List<CollectionInfo> collections;
List<JournalEntity> journals;
}
}
@ -231,8 +259,8 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
listCardDAV.setEnabled(!info.carddav.refreshing);
listCardDAV.setAlpha(info.carddav.refreshing ? 0.5f : 1);
AddressBookAdapter adapter = new AddressBookAdapter(this);
adapter.addAll(info.carddav.collections);
final CollectionListAdapter adapter = new CollectionListAdapter(this, account);
adapter.addAll(info.carddav.journals);
listCardDAV.setAdapter(adapter);
listCardDAV.setOnItemClickListener(onItemClickListener);
} else
@ -247,8 +275,8 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
listCalDAV.setEnabled(!info.caldav.refreshing);
listCalDAV.setAlpha(info.caldav.refreshing ? 0.5f : 1);
final CalendarAdapter adapter = new CalendarAdapter(this);
adapter.addAll(info.caldav.collections);
final CollectionListAdapter adapter = new CollectionListAdapter(this, account);
adapter.addAll(info.caldav.journals);
listCalDAV.setAdapter(adapter);
listCalDAV.setOnItemClickListener(onItemClickListener);
} else
@ -318,31 +346,23 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
public AccountInfo loadInBackground() {
AccountInfo info = new AccountInfo();
@Cleanup OpenHelper dbHelper = new OpenHelper(getContext());
SQLiteDatabase db = dbHelper.getReadableDatabase();
EntityDataStore<Persistable> data = ((App) getContext().getApplicationContext()).getData();
@Cleanup Cursor cursor = db.query(
Services._TABLE,
new String[] { Services.ID, Services.SERVICE },
Services.ACCOUNT_NAME + "=?", new String[] { account.name },
null, null, null);
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
String service = cursor.getString(1);
if (Services.SERVICE_CARDDAV.equals(service)) {
for (ServiceEntity serviceEntity : data.select(ServiceEntity.class).where(ServiceEntity.ACCOUNT.eq(account.name)).get()) {
long id = serviceEntity.getId();
CollectionInfo.Type service = serviceEntity.getType();
if (service.equals(CollectionInfo.Type.ADDRESS_BOOK)) {
info.carddav = new AccountInfo.ServiceInfo();
info.carddav.id = id;
info.carddav.refreshing = (davService != null && davService.isRefreshing(id)) || ContentResolver.isSyncActive(account, ContactsContract.AUTHORITY);
info.carddav.collections = JournalEntity.getCollections(data, id);
} else if (Services.SERVICE_CALDAV.equals(service)) {
info.carddav.journals = JournalEntity.getJournals(data, serviceEntity);
} else if (service.equals(CollectionInfo.Type.CALENDAR)) {
info.caldav = new AccountInfo.ServiceInfo();
info.caldav.id = id;
info.caldav.refreshing = (davService != null && davService.isRefreshing(id)) ||
ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) ||
ContentResolver.isSyncActive(account, TaskProvider.ProviderName.OpenTasks.authority);
info.caldav.collections = JournalEntity.getCollections(data, id);
info.caldav.journals = JournalEntity.getJournals(data, serviceEntity);
}
}
return info;
@ -352,17 +372,21 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
/* LIST ADAPTERS */
public static class AddressBookAdapter extends ArrayAdapter<CollectionInfo> {
public AddressBookAdapter(Context context) {
super(context, R.layout.account_carddav_item);
public static class CollectionListAdapter extends ArrayAdapter<JournalEntity> {
private Account account;
public CollectionListAdapter(Context context, Account account) {
super(context, R.layout.account_collection_item);
this.account = account;
}
@Override
public View getView(int position, View v, ViewGroup parent) {
if (v == null)
v = LayoutInflater.from(getContext()).inflate(R.layout.account_carddav_item, parent, false);
v = LayoutInflater.from(getContext()).inflate(R.layout.account_collection_item, parent, false);
final CollectionInfo info = getItem(position);
final JournalEntity journalEntity = getItem(position);
final CollectionInfo info = journalEntity.getInfo();
TextView tv = (TextView)v.findViewById(R.id.title);
tv.setText(TextUtils.isEmpty(info.displayName) ? info.uid : info.displayName);
@ -375,45 +399,22 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
tv.setText(info.description);
}
tv = (TextView)v.findViewById(R.id.read_only);
tv.setVisibility(info.readOnly ? View.VISIBLE : View.GONE);
return v;
}
}
public static class CalendarAdapter extends ArrayAdapter<CollectionInfo> {
public CalendarAdapter(Context context) {
super(context, R.layout.account_caldav_item);
}
@Override
public View getView(final int position, View v, ViewGroup parent) {
if (v == null)
v = LayoutInflater.from(getContext()).inflate(R.layout.account_caldav_item, parent, false);
final CollectionInfo info = getItem(position);
View vColor = v.findViewById(R.id.color);
if (info.color != null) {
vColor.setBackgroundColor(info.color);
final View vColor = v.findViewById(R.id.color);
if (info.type.equals(CollectionInfo.Type.ADDRESS_BOOK)) {
vColor.setVisibility(View.GONE);
} else {
vColor.setBackgroundColor(LocalCalendar.defaultColor);
if (info.color != null) {
vColor.setBackgroundColor(info.color);
} else {
vColor.setBackgroundColor(LocalCalendar.defaultColor);
}
}
TextView tv = (TextView)v.findViewById(R.id.title);
tv.setText(TextUtils.isEmpty(info.displayName) ? info.uid : info.displayName);
View readOnly = v.findViewById(R.id.read_only);
readOnly.setVisibility(info.readOnly ? View.VISIBLE : View.GONE);
tv = (TextView)v.findViewById(R.id.description);
if (TextUtils.isEmpty(info.description))
tv.setVisibility(View.GONE);
else {
tv.setVisibility(View.VISIBLE);
tv.setText(info.description);
}
tv = (TextView)v.findViewById(R.id.read_only);
tv.setVisibility(info.readOnly ? View.VISIBLE : View.GONE);
final View shared = v.findViewById(R.id.shared);
shared.setVisibility(account.name.equals(journalEntity.getOwner()) ? View.GONE : View.VISIBLE);
return v;
}

View File

@ -0,0 +1,172 @@
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);
memberPubKey = userInfoManager.get(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

@ -35,7 +35,9 @@ import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.journalmanager.JournalManager;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.JournalModel;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.ServiceEntity;
import io.requery.Persistable;
import io.requery.sql.EntityDataStore;
@ -125,31 +127,21 @@ public class CreateCollectionFragment extends DialogFragment implements LoaderMa
@Override
public Exception loadInBackground() {
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
try {
String authority = null;
// now insert collection into database:
SQLiteDatabase db = dbHelper.getWritableDatabase();
EntityDataStore<Persistable> data = ((App) getContext().getApplicationContext()).getData();
// 1. find service ID
String serviceType;
if (info.type == CollectionInfo.Type.ADDRESS_BOOK) {
serviceType = ServiceDB.Services.SERVICE_CARDDAV;
authority = ContactsContract.AUTHORITY;
} else if (info.type == CollectionInfo.Type.CALENDAR) {
serviceType = ServiceDB.Services.SERVICE_CALDAV;
authority = CalendarContract.AUTHORITY;
} else {
throw new IllegalArgumentException("Collection must be an address book or calendar");
}
@Cleanup Cursor c = db.query(ServiceDB.Services._TABLE, new String[]{ServiceDB.Services.ID},
ServiceDB.Services.ACCOUNT_NAME + "=? AND " + ServiceDB.Services.SERVICE + "=?",
new String[]{account.name, serviceType}, null, null, null
);
if (!c.moveToNext())
throw new IllegalStateException();
long serviceID = c.getLong(0);
ServiceEntity serviceEntity = JournalModel.Service.fetch(data, account.name, info.type);
AccountSettings settings = new AccountSettings(getContext(), account);
HttpUrl principal = HttpUrl.get(settings.getUri());
@ -167,8 +159,7 @@ public class CreateCollectionFragment extends DialogFragment implements LoaderMa
}
// 2. add collection to service
EntityDataStore<Persistable> data = ((App) getContext().getApplicationContext()).getData();
info.serviceID = serviceID;
info.serviceID = serviceEntity.getId();
JournalEntity journalEntity = JournalEntity.fetchOrCreate(data, info);
data.upsert(journalEntity);
@ -180,8 +171,6 @@ public class CreateCollectionFragment extends DialogFragment implements LoaderMa
return e;
} catch (Exceptions.IntegrityException|Exceptions.GenericCryptoException e) {
return e;
} finally {
dbHelper.close();
}
return null;

View File

@ -49,6 +49,7 @@ import com.etesync.syncadapter.journalmanager.Exceptions.HttpException;
import com.etesync.syncadapter.model.EntryEntity;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.ServiceEntity;
import io.requery.Persistable;
import io.requery.sql.EntityDataStore;
@ -246,8 +247,14 @@ public class DebugInfoActivity extends AppCompatActivity implements LoaderManage
dbHelper.dump(report);
report.append("\n");
report.append("JOURNALS DUMP\n");
report.append("SERVICES DUMP\n");
EntityDataStore<Persistable> data = ((App) getContext().getApplicationContext()).getData();
for (ServiceEntity serviceEntity : data.select(ServiceEntity.class).get()) {
report.append(serviceEntity.toString() + "\n");
}
report.append("\n");
report.append("JOURNALS DUMP\n");
List<JournalEntity> journals = data.select(JournalEntity.class).where(JournalEntity.DELETED.eq(false)).get().toList();
for (JournalEntity journal : journals) {
report.append(journal.toString() + "\n");

View File

@ -123,7 +123,7 @@ public class DeleteCollectionFragment extends DialogFragment implements LoaderMa
Crypto.CryptoManager crypto = new Crypto.CryptoManager(collectionInfo.version, settings.password(), collectionInfo.uid);
journalManager.deleteJournal(new JournalManager.Journal(crypto, collectionInfo.toJson(), collectionInfo.uid));
JournalEntity journalEntity = JournalEntity.fetch(data, collectionInfo.uid);
JournalEntity journalEntity = JournalEntity.fetch(data, collectionInfo.getServiceEntity(data), collectionInfo.uid);
journalEntity.setDeleted(true);
data.update(journalEntity);

View File

@ -78,7 +78,7 @@ public class EditCollectionActivity extends CreateCollectionActivity {
public void onDeleteCollection(MenuItem item) {
EntityDataStore<Persistable> data = ((App) getApplication()).getData();
int journalCount = data.count(JournalEntity.class).where(JournalEntity.SERVICE.eq(info.serviceID)).get().value();
int journalCount = data.count(JournalEntity.class).where(JournalEntity.SERVICE_MODEL.eq(info.getServiceEntity(data))).get().value();
if (journalCount < 2) {
new AlertDialog.Builder(this)

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

@ -10,11 +10,13 @@ package com.etesync.syncadapter.ui;
import android.accounts.Account;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.Gravity;
import android.view.Menu;
@ -50,7 +52,9 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh
EXTRA_COLLECTION_INFO = "collectionInfo";
private Account account;
private JournalEntity journalEntity;
protected CollectionInfo info;
private boolean isOwner;
public static Intent newIntent(Context context, Account account, CollectionInfo info) {
Intent intent = new Intent(context, ViewCollectionActivity.class);
@ -63,13 +67,14 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh
public void refresh() {
EntityDataStore<Persistable> data = ((App) getApplicationContext()).getData();
final JournalEntity journalEntity = JournalEntity.fetch(data, info.uid);
journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid);
if ((journalEntity == null) || journalEntity.isDeleted()) {
finish();
return;
}
info = journalEntity.getInfo();
isOwner = account.name.equals(journalEntity.getOwner());
final View colorSquare = findViewById(R.id.color);
if (info.type == CollectionInfo.Type.CALENDAR) {
@ -89,6 +94,13 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh
final TextView desc = (TextView) findViewById(R.id.description);
desc.setText(info.description);
final TextView owner = (TextView) findViewById(R.id.owner);
if (account.name.equals(journalEntity.getOwner())) {
owner.setVisibility(View.GONE);
} else {
owner.setText(getString(R.string.account_owner, journalEntity.getOwner()));
}
}
@Override
@ -159,13 +171,69 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh
}
public void onEditCollection(MenuItem item) {
startActivity(EditCollectionActivity.newIntent(this, account, info));
if (isOwner) {
startActivity(EditCollectionActivity.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.edit_owner_only, journalEntity.getOwner()))
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
}).create();
dialog.show();
}
}
public void onImport(MenuItem item) {
startActivity(ImportActivity.newIntent(ViewCollectionActivity.this, account, info));
}
public void onManageMembers(MenuItem item) {
if (info.type.equals(CollectionInfo.Type.ADDRESS_BOOK)) {
AlertDialog dialog = new AlertDialog.Builder(this)
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.not_allowed_title)
.setMessage(R.string.members_address_book_not_allowed)
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
}).create();
dialog.show();
} else if (info.version < 2) {
AlertDialog dialog = new AlertDialog.Builder(this)
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.not_allowed_title)
.setMessage(R.string.members_old_journals_not_allowed)
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
}).create();
dialog.show();
} else 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;
@ -173,7 +241,7 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh
protected Long doInBackground(Void... aVoids) {
EntityDataStore<Persistable> data = ((App) getApplicationContext()).getData();
final JournalEntity journalEntity = JournalEntity.fetch(data, info.uid);
final JournalEntity journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid);
entryCount = data.count(EntryEntity.class).where(EntryEntity.JOURNAL.eq(journalEntity)).get().value();
long count;

View File

@ -137,7 +137,7 @@ public class ListEntriesFragment extends ListFragment implements AdapterView.OnI
@Override
protected List<EntryEntity> doInBackground(Void... voids) {
journalEntity = JournalModel.Journal.fetch(data, info.uid);
journalEntity = JournalModel.Journal.fetch(data, info.getServiceEntity(data), info.uid);
return data.select(EntryEntity.class).where(EntryEntity.JOURNAL.eq(journalEntity)).orderBy(EntryEntity.ID.desc()).get().toList();
}

View File

@ -19,6 +19,7 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import com.etesync.syncadapter.HttpClient;
import com.etesync.syncadapter.journalmanager.Crypto;
import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.journalmanager.JournalAuthenticator;
import com.etesync.syncadapter.log.StringHandler;
@ -99,6 +100,7 @@ public class BaseConfigurationFinder {
public final String userName, authtoken;
public String rawPassword;
public String password;
public Crypto.AsymmetricKeyPair keyPair;
public final ServiceInfo cardDAV;
public final ServiceInfo calDAV;

View File

@ -14,7 +14,6 @@ import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
@ -26,23 +25,30 @@ import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import java.util.logging.Level;
import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App;
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.Exceptions;
import com.etesync.syncadapter.journalmanager.UserInfoManager;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.resource.LocalTaskList;
import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration;
import java.util.logging.Level;
import at.bitfire.ical4android.TaskProvider;
import io.requery.Persistable;
import io.requery.sql.EntityDataStore;
import lombok.Cleanup;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
public class SetupEncryptionFragment extends DialogFragment implements LoaderManager.LoaderCallbacks<Configuration> {
private static final String KEY_CONFIG = "config";
@ -113,6 +119,27 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan
@Override
public Configuration loadInBackground() {
config.password = Crypto.deriveKey(config.userName, config.rawPassword);
try {
Crypto.CryptoManager cryptoManager;
OkHttpClient httpClient = HttpClient.create(getContext(), config.authtoken);
UserInfoManager userInfoManager = new UserInfoManager(httpClient, HttpUrl.get(config.url));
UserInfoManager.UserInfo userInfo = userInfoManager.get(config.userName);
if (userInfo != null) {
App.log.info("Fetched userInfo for " + config.userName);
cryptoManager = new Crypto.CryptoManager(userInfo.getVersion(), config.password, "userInfo");
userInfo.verify(cryptoManager);
config.keyPair = new Crypto.AsymmetricKeyPair(userInfo.getContent(cryptoManager), userInfo.getPubkey());
}
} catch (Exceptions.HttpException e) {
e.printStackTrace();
} catch (Exceptions.IntegrityException e) {
e.printStackTrace();
} catch (Exceptions.VersionTooNewException e) {
e.printStackTrace();
}
return config;
}
}
@ -131,16 +158,17 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan
// add entries for account to service DB
App.log.log(Level.INFO, "Writing account configuration to database", config);
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
SQLiteDatabase db = dbHelper.getWritableDatabase();
try {
AccountSettings settings = new AccountSettings(getContext(), account);
settings.setAuthToken(config.authtoken);
if (config.keyPair != null) {
settings.setKeyPair(config.keyPair);
}
if (config.cardDAV != null) {
// insert CardDAV service
insertService(db, accountName, ServiceDB.Services.SERVICE_CARDDAV, config.cardDAV);
insertService(accountName, CollectionInfo.Type.ADDRESS_BOOK, config.cardDAV);
// contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
settings.setSyncInterval(ContactsContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL);
@ -150,7 +178,7 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan
if (config.calDAV != null) {
// insert CalDAV service
insertService(db, accountName, ServiceDB.Services.SERVICE_CALDAV, config.calDAV);
insertService(accountName, CollectionInfo.Type.CALENDAR, config.calDAV);
// calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
settings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL);
@ -172,22 +200,20 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan
return true;
}
protected long insertService(SQLiteDatabase db, String accountName, String service, BaseConfigurationFinder.Configuration.ServiceInfo info) {
ContentValues values = new ContentValues();
protected void insertService(String accountName, CollectionInfo.Type serviceType, BaseConfigurationFinder.Configuration.ServiceInfo info) {
EntityDataStore<Persistable> data = ((App) getContext().getApplicationContext()).getData();
// insert service
values.put(ServiceDB.Services.ACCOUNT_NAME, accountName);
values.put(ServiceDB.Services.SERVICE, service);
long serviceID = db.insertWithOnConflict(ServiceDB.Services._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
EntityDataStore<Persistable> data = ((App) getContext().getApplicationContext()).getData();
ServiceEntity serviceEntity = new ServiceEntity();
serviceEntity.setAccount(accountName);
serviceEntity.setType(serviceType);
data.upsert(serviceEntity);
// insert collections
for (CollectionInfo collection : info.collections.values()) {
collection.serviceID = serviceID;
JournalEntity journalEntity = new JournalEntity(collection);
collection.serviceID = serviceEntity.getId();
JournalEntity journalEntity = new JournalEntity(data, collection);
data.insert(journalEntity);
}
return serviceID;
}
}

View File

@ -0,0 +1,146 @@
package com.etesync.syncadapter.ui.setup;
import android.accounts.Account;
import android.app.Dialog;
import android.app.ProgressDialog;
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.Fragment;
import android.support.v7.app.AlertDialog;
import android.widget.TextView;
import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.HttpClient;
import com.etesync.syncadapter.InvalidAccountException;
import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Constants;
import com.etesync.syncadapter.journalmanager.Crypto;
import com.etesync.syncadapter.journalmanager.UserInfoManager;
import lombok.RequiredArgsConstructor;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import static com.etesync.syncadapter.Constants.KEY_ACCOUNT;
public class SetupUserInfoFragment extends DialogFragment {
private Account account;
private AccountSettings settings;
public static SetupUserInfoFragment newInstance(Account account) {
SetupUserInfoFragment frag = new SetupUserInfoFragment();
Bundle args = new Bundle(1);
args.putParcelable(KEY_ACCOUNT, account);
frag.setArguments(args);
return frag;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
ProgressDialog progress = new ProgressDialog(getActivity());
progress.setTitle(R.string.login_encryption_setup_title);
progress.setMessage(getString(R.string.login_encryption_setup));
progress.setIndeterminate(true);
progress.setCanceledOnTouchOutside(false);
setCancelable(false);
return progress;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
account = getArguments().getParcelable(KEY_ACCOUNT);
try {
settings = new AccountSettings(getContext(), account);
} catch (Exception e) {
e.printStackTrace();
}
new SetupUserInfo().execute(account);
}
public static boolean hasUserInfo(Context context, Account account) {
AccountSettings settings;
try {
settings = new AccountSettings(context, account);
} catch (InvalidAccountException e) {
e.printStackTrace();
return false;
}
return settings.getKeyPair() != null;
}
protected class SetupUserInfo extends AsyncTask<Account, Integer, SetupUserInfo.SetupUserInfoResult> {
ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
progressDialog = (ProgressDialog) getDialog();
}
@Override
protected SetupUserInfo.SetupUserInfoResult doInBackground(Account... accounts) {
try {
Crypto.CryptoManager cryptoManager;
OkHttpClient httpClient = HttpClient.create(getContext(), account);
UserInfoManager userInfoManager = new UserInfoManager(httpClient, HttpUrl.get(settings.getUri()));
UserInfoManager.UserInfo userInfo = userInfoManager.get(account.name);
if (userInfo == null) {
App.log.info("Creating userInfo for " + account.name);
cryptoManager = new Crypto.CryptoManager(Constants.CURRENT_VERSION, settings.password(), "userInfo");
userInfo = UserInfoManager.UserInfo.generate(cryptoManager, account.name);
userInfoManager.create(userInfo);
} else {
App.log.info("Fetched userInfo for " + account.name);
cryptoManager = new Crypto.CryptoManager(userInfo.getVersion(), settings.password(), "userInfo");
userInfo.verify(cryptoManager);
}
Crypto.AsymmetricKeyPair keyPair = new Crypto.AsymmetricKeyPair(userInfo.getContent(cryptoManager), userInfo.getPubkey());
return new SetupUserInfoResult(keyPair, null);
} catch (Exception e) {
e.printStackTrace();
return new SetupUserInfoResult(null, e);
}
}
@Override
protected void onPostExecute(SetupUserInfoResult result) {
if (result.exception == null) {
settings.setKeyPair(result.keyPair);
} else {
Dialog dialog = new AlertDialog.Builder(getActivity())
.setTitle(R.string.login_encryption_error_title)
.setIcon(R.drawable.ic_error_dark)
.setMessage(result.exception.getLocalizedMessage())
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// dismiss
}
})
.create();
dialog.show();
}
dismissAllowingStateLoss();
}
@RequiredArgsConstructor
class SetupUserInfoResult {
final Crypto.AsymmetricKeyPair keyPair;
final Exception exception;
}
}
}

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="M11.83,1.73C8.43,1.79 6.23,3.32 6.23,3.32C5.95,3.5 5.88,3.91 6.07,4.19C6.27,4.5 6.66,4.55 6.96,4.34C6.96,4.34 11.27,1.15 17.46,4.38C17.75,4.55 18.14,4.45 18.31,4.15C18.5,3.85 18.37,3.47 18.03,3.28C16.36,2.4 14.78,1.96 13.36,1.8C12.83,1.74 12.32,1.72 11.83,1.73M12.22,4.34C6.26,4.26 3.41,9.05 3.41,9.05C3.22,9.34 3.3,9.72 3.58,9.91C3.87,10.1 4.26,10 4.5,9.68C4.5,9.68 6.92,5.5 12.2,5.59C17.5,5.66 19.82,9.65 19.82,9.65C20,9.94 20.38,10.04 20.68,9.87C21,9.69 21.07,9.31 20.9,9C20.9,9 18.15,4.42 12.22,4.34M11.5,6.82C9.82,6.94 8.21,7.55 7,8.56C4.62,10.53 3.1,14.14 4.77,19C4.88,19.33 5.24,19.5 5.57,19.39C5.89,19.28 6.07,18.92 5.95,18.6V18.6C4.41,14.13 5.78,11.2 7.8,9.5C9.77,7.89 13.25,7.5 15.84,9.1C17.11,9.9 18.1,11.28 18.6,12.64C19.11,14 19.08,15.32 18.67,15.94C18.25,16.59 17.4,16.83 16.65,16.64C15.9,16.45 15.29,15.91 15.26,14.77C15.23,13.06 13.89,12 12.5,11.84C11.16,11.68 9.61,12.4 9.21,14C8.45,16.92 10.36,21.07 14.78,22.45C15.11,22.55 15.46,22.37 15.57,22.04C15.67,21.71 15.5,21.35 15.15,21.25C11.32,20.06 9.87,16.43 10.42,14.29C10.66,13.33 11.5,13 12.38,13.08C13.25,13.18 14,13.7 14,14.79C14.05,16.43 15.12,17.54 16.34,17.85C17.56,18.16 18.97,17.77 19.72,16.62C20.5,15.45 20.37,13.8 19.78,12.21C19.18,10.61 18.07,9.03 16.5,8.04C14.96,7.08 13.19,6.7 11.5,6.82M11.86,9.25V9.26C10.08,9.32 8.3,10.24 7.28,12.18C5.96,14.67 6.56,17.21 7.44,19.04C8.33,20.88 9.54,22.1 9.54,22.1C9.78,22.35 10.17,22.35 10.42,22.11C10.67,21.87 10.67,21.5 10.43,21.23C10.43,21.23 9.36,20.13 8.57,18.5C7.78,16.87 7.3,14.81 8.38,12.77C9.5,10.67 11.5,10.16 13.26,10.67C15.04,11.19 16.53,12.74 16.5,15.03C16.46,15.38 16.71,15.68 17.06,15.7C17.4,15.73 17.7,15.47 17.73,15.06C17.79,12.2 15.87,10.13 13.61,9.47C13.04,9.31 12.45,9.23 11.86,9.25M12.08,14.25C11.73,14.26 11.46,14.55 11.47,14.89C11.47,14.89 11.5,16.37 12.31,17.8C13.15,19.23 14.93,20.59 18.03,20.3C18.37,20.28 18.64,20 18.62,19.64C18.6,19.29 18.3,19.03 17.91,19.06C15.19,19.31 14.04,18.28 13.39,17.17C12.74,16.07 12.72,14.88 12.72,14.88C12.72,14.53 12.44,14.25 12.08,14.25Z"/>
</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

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2013 2016 Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
~ http://www.gnu.org/licenses/gpl.html
-->
<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="My Address Book"/>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="Address Book Description"/>
</LinearLayout>
<TextView
android:id="@+id/read_only"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/ic_remove_circle_dark"/>
</LinearLayout>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright © 2013 2016 Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
@ -8,18 +7,20 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:gravity="center_vertical">
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:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingLeft="12dp"
android:paddingRight="12dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="8dp"
android:orientation="vertical">
<TextView
@ -27,27 +28,36 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="My Calendar"/>
tools:text="Title" />
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="Calendar Description"/>
tools:text="Description" />
</LinearLayout>
<TextView
<ImageView
android:id="@+id/shared"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginRight="8dp"
android:src="@drawable/ic_members_dark"
android:visibility="gone" />
<ImageView
android:id="@+id/read_only"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/ic_remove_circle_dark"/>
android:layout_marginRight="8dp"
android:src="@drawable/ic_remove_circle_dark" />
<View
android:id="@+id/color"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginRight="4dp"
tools:background="@color/green700"/>
tools:background="@color/green700" />
</LinearLayout>

View File

@ -0,0 +1,43 @@
<?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:layout_marginBottom="16dp"
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"
tools:text="Description" />
<TextView
android:id="@+id/owner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Owner: some@email.com"/>
</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

@ -20,6 +20,11 @@
android:title="@string/account_settings"
app:showAsAction="ifRoom"/>
<item android:id="@+id/show_fingerprint"
android:icon="@drawable/ic_fingerprint_dark"
android:title="@string/account_show_fingerprint"
app:showAsAction="ifRoom"/>
<item android:id="@+id/delete_account"
android:title="@string/account_delete"
app:showAsAction="never"/>

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

@ -90,6 +90,7 @@
<string name="account_synchronizing_now">Synchronizing now</string>
<string name="account_settings">Account settings</string>
<string name="account_delete">Delete account</string>
<string name="account_show_fingerprint">Show Fingerprint</string>
<string name="account_delete_confirmation_title">Really delete account?</string>
<string name="account_delete_confirmation_text">All local copies of address books, calendars and task lists will be deleted.</string>
<string name="account_create_new_calendar">Create new calendar</string>
@ -97,9 +98,30 @@
<string name="account_delete_collection_last_text">Deleting the last collection is not allowed, please create a new one if you\'d like to delete this one.</string>
<string name="account_showcase_view_collection">You can click on an item to view the collection. From there you can view the journal, import, and much more...</string>
<string name="show_fingperprint_title">My Fingerprint</string>
<!-- 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="account_owner">Owner: %s</string>
<string name="members_owner_only">Only the owner of this collection (%s) is allowed to view its members.</string>
<string name="not_allowed_title">Not Allowed</string>
<string name="edit_owner_only">Only the owner of this collection (%s) is allowed to edit it.</string>
<string name="members_address_book_not_allowed">Sharing of address books is currently not supported.</string>
<string name="members_old_journals_not_allowed">Sharing of old-style journals is not allowed. In order to share this journal, create a new one, and copy its contents over using the \"import\" dialog. If you are experiencing any issues, please contact support.</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>
@ -140,6 +162,10 @@
<string name="login_encryption_setup_title">Setting up encryption</string>
<string name="login_encryption_setup">Please wait, setting up encryption…</string>
<!-- SetupUserInfoFragment -->
<string name="login_encryption_error_title">Encryption Error</string>
<!-- ImportFragment -->
<string name="import_dialog_title">Import</string>
<string name="import_dialog_failed_title">Import Failed</string>
@ -208,6 +234,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>

View File

@ -14,9 +14,11 @@ import org.apache.commons.codec.Charsets;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.spongycastle.util.encoders.Hex;
import java.io.IOException;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class EncryptionTest {
@ -67,4 +69,19 @@ public class EncryptionTest {
public void testCryptoVersionOutOfRange() throws Exceptions.IntegrityException, Exceptions.VersionTooNewException {
new Crypto.CryptoManager(999, Helpers.keyBase64, "TestSaltShouldBeJournalId");
}
@Test
public void testAsymCrypto() throws Exceptions.IntegrityException, Exceptions.GenericCryptoException {
Crypto.AsymmetricKeyPair keyPair = Crypto.generateKeyPair();
Crypto.AsymmetricCryptoManager cryptoManager = new Crypto.AsymmetricCryptoManager(keyPair);
byte[] clearText = "This Is Some Test Cleartext.".getBytes(Charsets.UTF_8);
byte[] cipher = cryptoManager.encrypt(keyPair.getPublicKey(), clearText);
byte[] clearText2 = cryptoManager.decrypt(cipher);
assertArrayEquals(clearText, clearText2);
// Mostly for coverage. Make sure it's the expected sha256 value.
assertEquals("ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
Hex.toHexString(Crypto.AsymmetricCryptoManager.getKeyFingerprint("a".getBytes(Charsets.UTF_8))).toLowerCase());
}
}

View File

@ -12,6 +12,7 @@ import com.etesync.syncadapter.App;
import com.etesync.syncadapter.HttpClient;
import com.etesync.syncadapter.model.CollectionInfo;
import org.apache.commons.codec.Charsets;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -31,6 +32,7 @@ import okio.BufferedSink;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
public class ServiceTest {
private OkHttpClient httpClient;
@ -91,7 +93,7 @@ public class ServiceTest {
}
assertNotNull(caught);
List<JournalManager.Journal> journals = journalManager.getJournals(Helpers.keyBase64);
List<JournalManager.Journal> journals = journalManager.getJournals();
assertEquals(journals.size(), 1);
CollectionInfo info2 = CollectionInfo.fromJson(journals.get(0).getContent(crypto));
assertEquals(info2.displayName, info.displayName);
@ -101,7 +103,7 @@ public class ServiceTest {
journal = new JournalManager.Journal(crypto, info.toJson(), info.uid);
journalManager.updateJournal(journal);
journals = journalManager.getJournals(Helpers.keyBase64);
journals = journalManager.getJournals();
assertEquals(journals.size(), 1);
info2 = CollectionInfo.fromJson(journals.get(0).getContent(crypto));
assertEquals(info2.displayName, info.displayName);
@ -109,7 +111,7 @@ public class ServiceTest {
// Delete journal
journalManager.deleteJournal(journal);
journals = journalManager.getJournals(Helpers.keyBase64);
journals = journalManager.getJournals();
assertEquals(journals.size(), 0);
// Bad HMAC
@ -122,7 +124,10 @@ public class ServiceTest {
try {
caught = null;
journalManager.getJournals(Helpers.keyBase64);
for (JournalManager.Journal journal1 : journalManager.getJournals()) {
Crypto.CryptoManager crypto1 = new Crypto.CryptoManager(info.version, Helpers.keyBase64, journal1.getUid());
journal1.verify(crypto1);
}
} catch (Exceptions.IntegrityException e) {
caught = e;
}
@ -196,4 +201,62 @@ public class ServiceTest {
}
assertNotNull(caught);
}
@Test
public void testUserInfo() throws IOException, Exceptions.HttpException, Exceptions.GenericCryptoException, Exceptions.IntegrityException {
Crypto.CryptoManager cryptoManager = new Crypto.CryptoManager(Constants.CURRENT_VERSION, Helpers.keyBase64, "userInfo");
UserInfoManager.UserInfo userInfo, userInfo2;
UserInfoManager manager = new UserInfoManager(httpClient, remote);
// Get when there's nothing
userInfo = manager.get(Helpers.USER);
assertNull(userInfo);
// Create
userInfo = UserInfoManager.UserInfo.generate(cryptoManager, Helpers.USER);
manager.create(userInfo);
// Get
userInfo2 = manager.get(Helpers.USER);
assertNotNull(userInfo2);
assertArrayEquals(userInfo.getContent(cryptoManager), userInfo2.getContent(cryptoManager));
// Update
userInfo.setContent(cryptoManager, "test".getBytes(Charsets.UTF_8));
manager.update(userInfo);
userInfo2 = manager.get(Helpers.USER);
assertNotNull(userInfo2);
assertArrayEquals(userInfo.getContent(cryptoManager), userInfo2.getContent(cryptoManager));
// Delete
manager.delete(userInfo);
userInfo = manager.get(Helpers.USER);
assertNull(userInfo);
}
@Test
public void testJournalMember() throws IOException, Exceptions.HttpException, Exceptions.GenericCryptoException, Exceptions.IntegrityException {
JournalManager journalManager = new JournalManager(httpClient, remote);
CollectionInfo info = CollectionInfo.defaultForServiceType(CollectionInfo.Type.ADDRESS_BOOK);
info.uid = JournalManager.Journal.genUid();
info.displayName = "Test";
Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, Helpers.keyBase64, info.uid);
JournalManager.Journal journal = new JournalManager.Journal(crypto, info.toJson(), info.uid);
journalManager.putJournal(journal);
assertEquals(journalManager.listMembers(journal).size(), 0);
// Test inviting ourselves
JournalManager.Member member = new JournalManager.Member(Helpers.USER, "test".getBytes(Charsets.UTF_8));
journalManager.addMember(journal, member);
assertEquals(journalManager.listMembers(journal).size(), 1);
// Uninviting ourselves
journalManager.deleteMember(journal, member);
assertEquals(journalManager.listMembers(journal).size(), 0);
}
}