1
0
mirror of https://github.com/etesync/android synced 2025-04-20 00:59:05 +00:00

Cache journals locally.

This is useful both as an anti-tampering mechanism, and will be used
later when interacting with the journal is implemented.
This commit is contained in:
Tom Hacohen 2017-03-09 00:19:33 +00:00
parent afcb00e4f1
commit 29fd177a95
8 changed files with 160 additions and 17 deletions

View File

@ -127,6 +127,10 @@ dependencies {
compile 'com.github.yukuku:ambilwarna:2.0.1'
compile 'io.requery:requery:1.2.0'
compile 'io.requery:requery-android:1.2.0'
annotationProcessor 'io.requery:requery-processor:1.2.0'
compile group: 'com.madgag.spongycastle', name: 'core', version: '1.54.0.0'
compile group: 'com.madgag.spongycastle', name: 'prov', version: '1.54.0.0'
compile group: 'com.google.code.gson', name: 'gson', version: '1.7.2'

View File

@ -18,12 +18,19 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Process;
import android.os.StrictMode;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v7.app.NotificationCompat;
import android.util.Log;
import com.etesync.syncadapter.log.LogcatHandler;
import com.etesync.syncadapter.log.PlainTextFormatter;
import com.etesync.syncadapter.model.Models;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.Settings;
import org.apache.commons.lang3.time.DateFormatUtils;
import java.io.File;
@ -36,10 +43,11 @@ import java.util.logging.Logger;
import javax.net.ssl.HostnameVerifier;
import at.bitfire.cert4android.CustomCertManager;
import com.etesync.syncadapter.log.LogcatHandler;
import com.etesync.syncadapter.log.PlainTextFormatter;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.Settings;
import io.requery.Persistable;
import io.requery.android.sqlite.DatabaseSource;
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;
@ -76,6 +84,7 @@ public class App extends Application {
super.onCreate();
reinitCertManager();
reinitLogger();
StrictMode.enableDefaults();
}
public void reinitCertManager() {
@ -176,4 +185,26 @@ public class App extends Application {
}
}
private EntityDataStore<Persistable> dataStore;
/**
* @return {@link EntityDataStore} single instance for the application.
* <p/>
* Note if you're using Dagger you can make this part of your application level module returning
* {@code @Provides @Singleton}.
*/
public EntityDataStore<Persistable> getData() {
if (dataStore == null) {
// override onUpgrade to handle migrating to a new version
DatabaseSource source = new DatabaseSource(this, Models.DEFAULT, 1);
if (BuildConfig.DEBUG) {
// use this in development mode to drop and recreate the tables on every upgrade
source.setTableCreationMode(TableCreationMode.DROP_CREATE);
}
Configuration configuration = source.getConfiguration();
dataStore = new EntityDataStore<>(configuration);
}
return dataStore;
}
}

View File

@ -7,6 +7,8 @@ import java.util.List;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.GsonHelper;
import lombok.Getter;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@ -15,11 +17,14 @@ import okhttp3.Response;
import okhttp3.ResponseBody;
public class JournalEntryManager extends BaseManager {
@Getter
final String uid;
final static private Type entryType = new TypeToken<List<Entry>>() {
}.getType();
public JournalEntryManager(OkHttpClient httpClient, HttpUrl remote, String journal) {
this.uid = journal;
this.remote = remote.newBuilder()
.addPathSegments("api/v1/journal")
.addPathSegments(journal)

View File

@ -0,0 +1,42 @@
package com.etesync.syncadapter.model;
import io.requery.CascadeAction;
import io.requery.Column;
import io.requery.Entity;
import io.requery.ForeignKey;
import io.requery.Generated;
import io.requery.Index;
import io.requery.Key;
import io.requery.ManyToOne;
import io.requery.OneToMany;
import io.requery.OneToOne;
import io.requery.ReferentialAction;
public class JournalModel {
@Entity
public static abstract class Journal {
@Key
@Generated
int id;
@Column(length = 64, unique = true, nullable = false)
String uid;
}
@Entity
public static abstract class Entry {
@Key
@Generated
int id;
@Column(length = 64, unique = true, nullable = false)
String uid;
String content;
@Index("journal_index")
@ForeignKey(update = ReferentialAction.CASCADE)
@ManyToOne
Journal journal;
}
}

View File

@ -60,7 +60,10 @@ public class CalendarSyncManager extends SyncManager {
}
@Override
protected boolean prepare() throws ContactsStorageException {
protected boolean prepare() throws ContactsStorageException, CalendarStorageException {
if (!super.prepare())
return false;
journal = new JournalEntryManager(httpClient, remote, localCalendar().getName());
return true;
}

View File

@ -78,6 +78,8 @@ public class ContactsSyncManager extends SyncManager {
@Override
protected boolean prepare() throws ContactsStorageException, CalendarStorageException {
if (!super.prepare())
return false;
// prepare local address book
localCollection = new LocalAddressBook(account, provider);
LocalAddressBook localAddressBook = localAddressBook();

View File

@ -47,8 +47,12 @@ import com.etesync.syncadapter.R;
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.ServiceDB;
import com.etesync.syncadapter.ui.PermissionsActivity;
import io.requery.Persistable;
import io.requery.sql.EntityDataStore;
import lombok.Cleanup;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
@ -195,6 +199,7 @@ public abstract class SyncAdapterService extends Service {
}
private void saveCollections(SQLiteDatabase db, Iterable<CollectionInfo> collections) {
EntityDataStore<Persistable> data = ((App) context.getApplicationContext()).getData();
Long service = dbHelper.getService(db, account, serviceType.toString());
db.delete(ServiceDB.Collections._TABLE, ServiceDB.Collections.SERVICE_ID + "=?", new String[]{String.valueOf(service)});
for (CollectionInfo collection : collections) {
@ -202,6 +207,13 @@ public abstract class SyncAdapterService extends Service {
App.log.log(Level.FINE, "Saving collection", values);
values.put(ServiceDB.Collections.SERVICE_ID, service);
db.insertWithOnConflict(ServiceDB.Collections._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
JournalEntity journalEntity = data.select(JournalEntity.class).where(JournalEntity.UID.eq(collection.url)).limit(1).get().firstOrNull();
if (journalEntity == null) {
journalEntity = new JournalEntity();
journalEntity.setUid(collection.url);
data.insert(journalEntity);
}
}
}
}

View File

@ -14,16 +14,6 @@ import android.content.Intent;
import android.content.SyncResult;
import android.os.Bundle;
import org.apache.commons.collections4.ListUtils;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.Constants;
@ -35,12 +25,27 @@ import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Exceptions;
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.resource.LocalCollection;
import com.etesync.syncadapter.resource.LocalResource;
import com.etesync.syncadapter.ui.DebugInfoActivity;
import org.apache.commons.collections4.ListUtils;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.InvalidCalendarException;
import at.bitfire.vcard4android.ContactsStorageException;
import io.requery.Persistable;
import io.requery.sql.EntityDataStore;
import lombok.Getter;
import okhttp3.OkHttpClient;
@ -63,6 +68,9 @@ abstract public class SyncManager {
protected OkHttpClient httpClient;
protected JournalEntryManager journal;
private JournalEntity _journalEntity;
private EntityDataStore<Persistable> data;
/**
* remote CTag (uuid of the last entry on the server). We update it when we fetch/push and save when everything works.
@ -101,6 +109,8 @@ abstract public class SyncManager {
this.uniqueCollectionId = uniqueCollectionId;
notificationManager = new NotificationHelper(context, uniqueCollectionId, notificationId());
notificationManager.cancel();
data = ((App) context.getApplicationContext()).getData();
}
protected abstract int notificationId();
@ -207,10 +217,26 @@ abstract public class SyncManager {
* @return whether actual synchronization is required / can be made. true = synchronization
* shall be continued, false = synchronization can be skipped
*/
abstract protected boolean prepare() throws ContactsStorageException, CalendarStorageException;
protected boolean prepare() throws ContactsStorageException, CalendarStorageException {
return true;
}
abstract protected void processSyncEntry(SyncEntry cEntry) throws IOException, ContactsStorageException, CalendarStorageException, InvalidCalendarException;
private JournalEntity getJournalEntity() {
if (_journalEntity == null)
_journalEntity = data.select(JournalEntity.class).where(JournalEntity.UID.eq(journal.getUid())).limit(1).get().first();
return _journalEntity;
}
private void persistSyncEntry(String uid, SyncEntry syncEntry) {
EntryEntity entry = new EntryEntity();
entry.setUid(uid);
entry.setContent(syncEntry.toJson());
entry.setJournal(getJournalEntity());
data.insert(entry);
}
protected void applyLocalEntries() throws IOException, ContactsStorageException, CalendarStorageException, Exceptions.HttpException, InvalidCalendarException, InterruptedException {
// FIXME: Need a better strategy
// We re-apply local entries so our changes override whatever was written in the remote.
@ -225,6 +251,7 @@ abstract public class SyncManager {
App.log.info("Processing (" + String.valueOf(i) + "/" + strTotal + ") " + entry.toString());
SyncEntry cEntry = SyncEntry.fromJournalEntry(settings.password(), entry);
persistSyncEntry(entry.getUuid(), cEntry);
if (cEntry.isAction(SyncEntry.Actions.DELETE)) {
continue;
}
@ -237,7 +264,23 @@ abstract public class SyncManager {
}
protected void fetchEntries() throws Exceptions.HttpException, ContactsStorageException, CalendarStorageException, Exceptions.IntegrityException {
remoteEntries = journal.getEntries(settings.password(), remoteCTag);
int count = data.count(EntryEntity.class).where(EntryEntity.JOURNAL.eq(getJournalEntity())).get().value();
if ((remoteCTag != null) && (count == 0)) {
// If we are updating an existing installation with no saved journal, we need to add
remoteEntries = journal.getEntries(settings.password(), null);
int i = 0;
for (JournalEntryManager.Entry entry : remoteEntries){
SyncEntry cEntry = SyncEntry.fromJournalEntry(settings.password(), entry);
persistSyncEntry(entry.getUuid(), cEntry);
i++;
if (remoteCTag.equals(entry.getUuid())) {
remoteEntries.subList(0, i).clear();
break;
}
}
} else {
remoteEntries = journal.getEntries(settings.password(), remoteCTag);
}
App.log.info("Fetched " + String.valueOf(remoteEntries.size()) + " entries");
}
@ -256,6 +299,7 @@ abstract public class SyncManager {
App.log.info("Processing (" + String.valueOf(i) + "/" + strTotal + ") " + entry.toString());
SyncEntry cEntry = SyncEntry.fromJournalEntry(settings.password(), entry);
persistSyncEntry(entry.getUuid(), cEntry);
App.log.info("Processing resource for journal entry");
processSyncEntry(cEntry);