Skip to content

Commit

Permalink
Fix #13 -- Add asynchronous scanning
Browse files Browse the repository at this point in the history
Add greenDAO

More settings foo, SyncService

Download data

Offline scanning

Upload scan queue

Auto sync

Ignore missing translations during linting

be more web 2.0

API version handling

Switch from greenDAO to requery

Improve synchronization speed

Prevent race condition between threads

Display sync status
  • Loading branch information
raphaelm committed May 6, 2017
1 parent d218428 commit ec690e9
Show file tree
Hide file tree
Showing 18 changed files with 827 additions and 23 deletions.
4 changes: 4 additions & 0 deletions pretixdroid/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ android {

lintOptions {
disable 'InvalidPackage' // problem with jetty and bouncycastle
disable 'MissingTranslation'
}

signingConfigs {
Expand Down Expand Up @@ -70,6 +71,9 @@ dependencies {
compile 'com.android.support:support-v4:25.3.1'
compile 'com.android.support:support-vector-drawable:25.3.1'
compile 'com.joshdholtz.sentry:sentry-android:1.6.0'
compile 'io.requery:requery:1.3.1'
compile 'io.requery:requery-android:1.3.1'
annotationProcessor 'io.requery:requery-processor:1.3.1'
compile 'com.facebook.stetho:stetho:1.5.0'
compile 'com.facebook.stetho:stetho-okhttp3:1.5.0'
}
4 changes: 4 additions & 0 deletions pretixdroid/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="eu.pretix.pretixdroid.ui.MainActivity" />
</activity>

<service
android:name=".async.SyncService"
android:exported="false" />
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@
import android.content.SharedPreferences;
import android.preference.PreferenceManager;

import eu.pretix.pretixdroid.net.api.PretixApi;

public class AppConfig {
public static final String PREFS_NAME = "pretixdroid";
public static final String PREFS_KEY_API_URL = "pretix_api_url";
public static final String PREFS_KEY_API_KEY = "pretix_api_key";
public static final String PREFS_KEY_API_VERSION = "pretix_api_version";
public static final String PREFS_KEY_FLASHLIGHT = "flashlight";
public static final String PREFS_KEY_AUTOFOCUS = "autofocus";
public static final String PREFS_KEY_CAMERA = "camera";
public static final String PREFS_KEY_ASYNC_MODE = "async";
public static final String PREFS_PLAY_AUDIO = "playaudio";
public static final String PREFS_KEY_LAST_SYNC = "last_sync";
public static final String PREFS_KEY_LAST_FAILED_SYNC = "last_failed_sync";
public static final String PREFS_KEY_LAST_FAILED_SYNC_MSG = "last_failed_sync_msg";
public static final String PREFS_KEY_LAST_DOWNLOAD = "last_download";
private SharedPreferences prefs;
private SharedPreferences default_prefs;

Expand All @@ -24,20 +32,32 @@ public boolean isConfigured() {
return prefs.contains(PREFS_KEY_API_URL);
}

public void setEventConfig(String url, String key) {
public void setEventConfig(String url, String key, int version) {
prefs.edit()
.putString(PREFS_KEY_API_URL, url)
.putString(PREFS_KEY_API_KEY, key)
.putInt(PREFS_KEY_API_VERSION, version)
.remove(PREFS_KEY_LAST_DOWNLOAD)
.remove(PREFS_KEY_LAST_SYNC)
.remove(PREFS_KEY_LAST_FAILED_SYNC)
.apply();
}

public void resetEventConfig() {
prefs.edit()
.remove(PREFS_KEY_API_URL)
.remove(PREFS_KEY_API_KEY)
.remove(PREFS_KEY_API_VERSION)
.remove(PREFS_KEY_LAST_DOWNLOAD)
.remove(PREFS_KEY_LAST_SYNC)
.remove(PREFS_KEY_LAST_FAILED_SYNC)
.apply();
}

public int getApiVersion() {
return prefs.getInt(PREFS_KEY_API_VERSION, PretixApi.SUPPORTED_API_VERSION);
}

public String getApiUrl() {
return prefs.getString(PREFS_KEY_API_URL, "");
}
Expand Down Expand Up @@ -77,4 +97,44 @@ public void setSoundEnabled(boolean val) {
public void setCamera(boolean val) {
default_prefs.edit().putBoolean(PREFS_KEY_CAMERA, val).apply();
}

public boolean getAsyncModeEnabled() {
return default_prefs.getBoolean(PREFS_KEY_ASYNC_MODE, false);
}

public void setAsyncModeEnabled(boolean val) {
default_prefs.edit().putBoolean(PREFS_KEY_ASYNC_MODE, val).apply();
}

public long getLastDownload() {
return prefs.getLong(PREFS_KEY_LAST_DOWNLOAD, 0);
}

public void setLastDownload(long val) {
prefs.edit().putLong(PREFS_KEY_LAST_DOWNLOAD, val).apply();
}

public long getLastSync() {
return prefs.getLong(PREFS_KEY_LAST_SYNC, 0);
}

public void setLastSync(long val) {
prefs.edit().putLong(PREFS_KEY_LAST_SYNC, val).apply();
}

public long getLastFailedSync() {
return prefs.getLong(PREFS_KEY_LAST_FAILED_SYNC, 0);
}

public void setLastFailedSync(long val) {
prefs.edit().putLong(PREFS_KEY_LAST_FAILED_SYNC, val).apply();
}

public String getLastFailedSyncMsg() {
return prefs.getString(PREFS_KEY_LAST_FAILED_SYNC_MSG, "");
}

public void setLastFailedSyncMsg(String val) {
prefs.edit().putString(PREFS_KEY_LAST_FAILED_SYNC_MSG, val).apply();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@

import com.facebook.stetho.Stetho;


import eu.pretix.pretixdroid.check.AsyncCheckProvider;
import eu.pretix.pretixdroid.check.OnlineCheckProvider;
import eu.pretix.pretixdroid.check.TicketCheckProvider;
import eu.pretix.pretixdroid.db.Models;
import io.requery.BlockingEntityStore;
import io.requery.Persistable;
import io.requery.android.sqlite.DatabaseSource;
import io.requery.sql.Configuration;
import io.requery.sql.EntityDataStore;

public class PretixDroid extends Application {
/*
* It is not a security problem that the keystore password is hardcoded in plain text.
Expand All @@ -12,6 +23,7 @@ public class PretixDroid extends Application {
* screwed either way.
*/
public static final String KEYSTORE_PASSWORD = "ZnDNUkQ01PVZyD7oNP3a8DVXrvltxD";
private BlockingEntityStore<Persistable> dataStore;

@Override
public void onCreate() {
Expand All @@ -22,7 +34,22 @@ public void onCreate() {
}
}

public DaoSession getDaoSession() {
return daoSession;
public BlockingEntityStore<Persistable> getData() {
if (dataStore == null) {
// override onUpgrade to handle migrating to a new version
DatabaseSource source = new DatabaseSource(this, Models.DEFAULT, 1);
Configuration configuration = source.getConfiguration();
dataStore = new EntityDataStore<Persistable>(configuration);
}
return dataStore;
}

public TicketCheckProvider getNewCheckProvider() {
AppConfig config = new AppConfig(this);
if (config.getAsyncModeEnabled()) {
return new AsyncCheckProvider(this);
} else {
return new OnlineCheckProvider(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package eu.pretix.pretixdroid.async;


public class SyncException extends Exception {
public SyncException(String msg) {
super(msg);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package eu.pretix.pretixdroid.async;

import android.app.IntentService;
import android.content.Intent;
import android.util.Log;

import com.joshdholtz.sentry.Sentry;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import eu.pretix.pretixdroid.AppConfig;
import eu.pretix.pretixdroid.PretixDroid;
import eu.pretix.pretixdroid.db.QueuedCheckIn;
import eu.pretix.pretixdroid.db.Ticket;
import eu.pretix.pretixdroid.net.api.ApiException;
import eu.pretix.pretixdroid.net.api.PretixApi;
import io.requery.BlockingEntityStore;
import io.requery.Persistable;
import io.requery.util.CloseableIterator;

public class SyncService extends IntentService {

private AppConfig config;
private PretixApi api;
private BlockingEntityStore<Persistable> dataStore;

public SyncService() {
super("SyncService");
}

@Override
public void onCreate() {
super.onCreate();
config = new AppConfig(this);
dataStore = ((PretixDroid) getApplicationContext()).getData();
}

@Override
protected void onHandleIntent(Intent workIntent) {
Log.i("SyncService", "Sync triggered");

// Rebuild in case config has changed
api = PretixApi.fromConfig(config);

if (!config.isConfigured()) {
return;
}

long upload_interval = 1000;
long download_interval = 30000;
if (!config.getAsyncModeEnabled()) {
download_interval = 120000;
}

if ((System.currentTimeMillis() - config.getLastSync()) < upload_interval) {
return;
}
if ((System.currentTimeMillis() - config.getLastFailedSync()) < 30000) {
return;
}

try {
uploadTicketData();

if ((System.currentTimeMillis() - config.getLastDownload()) > download_interval) {
downloadTicketData();
config.setLastDownload(System.currentTimeMillis());
}

config.setLastSync(System.currentTimeMillis());
config.setLastFailedSync(0);
} catch (SyncException e) {
config.setLastFailedSync(System.currentTimeMillis());
config.setLastFailedSyncMsg(e.getMessage());
}
}

private void uploadTicketData() throws SyncException {
Sentry.addBreadcrumb("sync.queue", "Start upload");

List<QueuedCheckIn> queued = dataStore.select(QueuedCheckIn.class).get().toList();

try {
for (QueuedCheckIn qci : queued) {
JSONObject response = api.redeem(qci.getSecret(), qci.getDatetime(), true, qci.getNonce());
String status = response.getString("status");
if ("ok".equals(status)) {
dataStore.delete(qci);
} else {
String reason = response.optString("reason");
if ("already_redeemed".equals(reason)) {
// Well, we can't really do something about this.
dataStore.delete(qci);
} // Else: Retry later
}
}
} catch (JSONException e) {
Sentry.captureException(e);
throw new SyncException("Unknown server response");
} catch (ApiException e) {
Sentry.addBreadcrumb("sync.tickets", "API Error: " + e.getMessage());
throw new SyncException(e.getMessage());
}
Sentry.addBreadcrumb("sync.queue", "Upload complete");
}

private static boolean string_changed(String newstring, String oldstring) {
return (newstring != null && oldstring == null)
|| (newstring == null && oldstring != null)
|| (newstring != null && oldstring != null && !newstring.equals(oldstring));
}

private void downloadTicketData() throws SyncException {
Sentry.addBreadcrumb("sync.tickets", "Start download");

// Download objects from server
JSONObject response;
try {
response = api.download();
} catch (ApiException e) {
Sentry.addBreadcrumb("sync.tickets", "API Error: " + e.getMessage());
throw new SyncException(e.getMessage());
}

// Index all known objects
Map<String, Ticket> known = new HashMap<>();
CloseableIterator<Ticket> tickets = dataStore.select(Ticket.class).get().iterator();
try {
while (tickets.hasNext()) {
Ticket t = tickets.next();
known.put(t.getSecret(), t);
}
} finally {
tickets.close();
}

try {
List<Ticket> inserts = new ArrayList<>();
// Insert or update
for (int i = 0; i < response.getJSONArray("results").length(); i++) {
JSONObject res = response.getJSONArray("results").getJSONObject(i);

Ticket ticket;
boolean created = false;
if (!known.containsKey(res.getString("secret"))) {
ticket = new Ticket();
created = true;
} else {
ticket = known.get(res.getString("secret"));
}

if (string_changed(res.getString("attendee_name"), ticket.getAttendee_name())) {
ticket.setAttendee_name(res.getString("attendee_name"));
}
if (string_changed(res.getString("item"), ticket.getItem())) {
ticket.setItem(res.getString("item"));
}
if (string_changed(res.getString("variation"), ticket.getVariation())) {
ticket.setVariation(res.getString("variation"));
}
if (string_changed(res.getString("order"), ticket.getOrder())) {
ticket.setOrder(res.getString("order"));
}
if (string_changed(res.getString("secret"), ticket.getSecret())) {
ticket.setSecret(res.getString("secret"));
}
if (res.getBoolean("redeemed") != ticket.isRedeemed()) {
ticket.setRedeemed(res.getBoolean("redeemed"));
}
if (res.getBoolean("paid") != ticket.isPaid()) {
ticket.setPaid(res.getBoolean("paid"));
}

if (created) {
inserts.add(ticket);
} else {
dataStore.update(ticket);
}
known.remove(res.getString("secret"));
}

dataStore.insert(inserts);
} catch (JSONException e) {
Sentry.captureException(e);
throw new SyncException("Unknown server response");
}

// Those have been deleted online, delete them here as well
for (String key : known.keySet()) {
dataStore.delete(known.get(key));
}
Sentry.addBreadcrumb("sync.tickets", "Download complete");
}

}
Loading

0 comments on commit ec690e9

Please sign in to comment.