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:
commit
87af98f92d
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"/>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
10
app/src/main/res/drawable/ic_account_add_dark.xml
Normal file
10
app/src/main/res/drawable/ic_account_add_dark.xml
Normal 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>
|
5
app/src/main/res/drawable/ic_fingerprint_dark.xml
Normal file
5
app/src/main/res/drawable/ic_fingerprint_dark.xml
Normal 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>
|
5
app/src/main/res/drawable/ic_members_dark.xml
Normal file
5
app/src/main/res/drawable/ic_members_dark.xml
Normal 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>
|
@ -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>
|
@ -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>
|
43
app/src/main/res/layout/collection_header.xml
Normal file
43
app/src/main/res/layout/collection_header.xml
Normal 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>
|
21
app/src/main/res/layout/collection_members_list.xml
Normal file
21
app/src/main/res/layout/collection_members_list.xml
Normal 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>
|
24
app/src/main/res/layout/collection_members_list_item.xml
Normal file
24
app/src/main/res/layout/collection_members_list_item.xml
Normal 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>
|
@ -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"
|
||||
|
61
app/src/main/res/layout/view_collection_members.xml
Normal file
61
app/src/main/res/layout/view_collection_members.xml
Normal 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>
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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>
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user