package org.ohmage.service;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.text.TextUtils;
import com.commonsware.cwac.wakeful.WakefulIntentService;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.ohmage.AccountHelper;
import org.ohmage.ConfigHelper;
import org.ohmage.NotificationHelper;
import org.ohmage.OhmageApi;
import org.ohmage.OhmageApi.UploadResponse;
import org.ohmage.UserPreferencesHelper;
import org.ohmage.logprobe.Analytics;
import org.ohmage.logprobe.Log;
import org.ohmage.logprobe.LogProbe.Status;
import org.ohmage.probemanager.DbContract.BaseProbeColumns;
import org.ohmage.probemanager.DbContract.Probes;
import org.ohmage.probemanager.DbContract.Responses;
import java.util.ArrayList;
import java.util.HashMap;
public class ProbeUploadService extends WakefulIntentService {
/** Extra to tell the upload service if it is running in the background **/
public static final String EXTRA_BACKGROUND = "is_background";
/** Uploaded in batches of 0.5 mb */
private static final int BATCH_SIZE = 1024 * 1024 / 2;
private static final String TAG = "ProbeUploadService";
public static final String PROBE_UPLOAD_STARTED = "org.ohmage.PROBE_UPLOAD_STARTED";
public static final String PROBE_UPLOAD_FINISHED = "org.ohmage.PROBE_UPLOAD_FINISHED";
public static final String PROBE_UPLOAD_ERROR = "org.ohmage.PROBE_UPLOAD_ERROR";
public static final String RESPONSE_UPLOAD_STARTED = "org.ohmage.RESPONSE_UPLOAD_STARTED";
public static final String RESPONSE_UPLOAD_FINISHED = "org.ohmage.RESPONSE_UPLOAD_FINISHED";
public static final String RESPONSE_UPLOAD_ERROR = "org.ohmage.RESPONSE_UPLOAD_ERROR";
public static final String PROBE_UPLOAD_SERVICE_FINISHED = "org.ohmage.PROBE_UPLOAD_SERVICE_FINISHED";
public static final String EXTRA_PROBE_ERROR = "extra_probe_error";
private OhmageApi mApi;
private boolean isBackground;
/**
* Set to true if there was an error uploading data
*/
private boolean mError = false;
private AccountHelper mAccount;
private UserPreferencesHelper mPrefs;
public ProbeUploadService() {
super(TAG);
}
@Override
public void onCreate() {
super.onCreate();
Analytics.service(this, Status.ON);
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
}
@Override
public void onDestroy() {
super.onDestroy();
Analytics.service(this, Status.OFF);
}
@Override
protected void doWakefulWork(Intent intent) {
mAccount = new AccountHelper(ProbeUploadService.this);
mPrefs = new UserPreferencesHelper(this);
if (mApi == null)
setOhmageApi(new OhmageApi(this));
isBackground = intent.getBooleanExtra(EXTRA_BACKGROUND, false);
Log.d(TAG, "upload probes");
ProbesUploader probesUploader = new ProbesUploader();
probesUploader.upload();
Log.d(TAG, "upload responses");
ResponsesUploader responsesUploader = new ResponsesUploader();
responsesUploader.upload();
// If there were no internal errors, we can say it was successful
if (!probesUploader.hadError() && !responsesUploader.hadError())
mPrefs.putLastProbeUploadTimestamp(System.currentTimeMillis());
sendBroadcast(new Intent(ProbeUploadService.PROBE_UPLOAD_SERVICE_FINISHED));
}
public void setOhmageApi(OhmageApi api) {
mApi = api;
}
/**
* Abstraction to upload object from the probes db. Uploads data in chunks
* based on the {@link #getName(Cursor)} and {@link #getVersion(Cursor)}
* values.
*
* @author cketcham
*/
public abstract class Uploader {
protected JsonParser mParser;
public Uploader() {
mParser = new JsonParser();
}
protected abstract Uri getContentURI();
protected abstract UploadResponse uploadCall(String serverUrl, String username,
String password, String client, String name, String version, JsonArray data);
protected abstract void uploadStarted();
protected abstract void uploadFinished();
protected abstract void uploadError(String string);
/**
* Adds a probe to the json array
*
* @param probes
* @param c
* @return the number of bytes in the payload
*/
protected abstract int addProbe(JsonArray probes, Cursor c);
protected abstract int getVersionIndex();
protected abstract int getNameIndex();
protected abstract String getVersionColumn();
protected abstract String getNameColumn();
protected abstract String[] getProjection();
public void upload() {
uploadStarted();
Cursor observersCursor = getContentResolver().query(getContentURI(), new String[] {
"distinct " + getNameColumn(), getVersionColumn()
}, BaseProbeColumns.USERNAME + "=?", new String[] {
mAccount.getUsername()
}, null);
HashMap<String, String> observers = new HashMap<String, String>();
while (observersCursor.moveToNext()) {
observers.put(observersCursor.getString(0), observersCursor.getString(1));
}
observersCursor.close();
for (String currentObserver : observers.keySet()) {
String currentVersion = observers.get(currentObserver);
Cursor c = getContentResolver().query(
getContentURI(),
getProjection(),
BaseProbeColumns.USERNAME + "=? AND " + getNameColumn() + "=? AND "
+ getVersionColumn() + "=?", new String[] {
mAccount.getUsername(), currentObserver, currentVersion
}, null);
JsonArray probes = new JsonArray();
ArrayList<Long> delete = new ArrayList<Long>();
StringBuilder deleteString = new StringBuilder();
int payloadSize = 0;
for (int i = 0; i < c.getCount() + 1; i++) {
try {
c.moveToPosition(i);
} catch (IllegalStateException e) {
// Due to a bug in 4.0 and greater(?) a crash can occur
// during the move.
// There is no good way to recover so we just restart
// More info here:
// http://code.google.com/p/android/issues/detail?id=32472
Log.e(TAG,
"illegal state exception moving to " + i + " of "
+ (c.getCount() + 1));
// Lets restart!
upload();
return;
}
// If we have a batch, upload all
// the points we have so far
if (payloadSize > BATCH_SIZE || c.isAfterLast()) {
Log.d(TAG, "total payload for " + currentObserver + "=" + payloadSize);
if (!upload(probes, currentObserver, currentVersion)) {
c.close();
return;
}
// Deleting this batch of points. We can only delete
// with a
// maximum expression tree depth of 1000
for (int batch = 0; batch < delete.size(); batch++) {
if (deleteString.length() != 0)
deleteString.append(" OR ");
deleteString.append(BaseColumns._ID + "=" + delete.get(batch));
// If we have 1000 Expressions or we are at the last
// point, delete them
if ((batch != 0 && batch % (1000 - 2) == 0)
|| batch == delete.size() - 1) {
getContentResolver().delete(getContentURI(),
deleteString.toString(), null);
deleteString = new StringBuilder();
}
}
delete.clear();
if (c.isAfterLast())
break;
payloadSize = 0;
probes = new JsonArray();
}
payloadSize += addProbe(probes, c);
delete.add(c.getLong(0));
}
c.close();
}
uploadFinished();
}
/**
* Uploads probes to the server
*
* @param probes the probe json
* @param c the cursor object
* @return false only if there was an error which indicates we shouldn't
* continue uploading
*/
private boolean upload(JsonArray probes, String observerId, String observerVersion) {
String username = mAccount.getUsername();
String hashedPassword = mAccount.getAuthToken();
// If there are no probes to upload just return successful
if (probes.size() > 0) {
UploadResponse response = uploadCall(ConfigHelper.serverUrl(), username,
hashedPassword, OhmageApi.CLIENT_NAME, observerId, observerVersion, probes);
response.handleError(ProbeUploadService.this);
if (response.getResult().equals(OhmageApi.Result.FAILURE)) {
if (response.hasAuthError())
return false;
mError = true;
uploadError(observerId + response.getErrorCodes().toString());
Log.d(TAG, "failed probes: " + probes.toString());
} else if (!response.getResult().equals(OhmageApi.Result.SUCCESS)) {
mError = true;
uploadError(null);
return false;
}
}
return true;
}
public boolean hadError() {
return mError;
}
}
private interface ProbeQuery {
static final String[] PROJECTION = new String[] {
Probes._ID, Probes.OBSERVER_ID, Probes.OBSERVER_VERSION, Probes.STREAM_ID,
Probes.STREAM_VERSION, Probes.PROBE_METADATA, Probes.PROBE_DATA
};
static final int OBSERVER_ID = 1;
static final int OBSERVER_VERSION = 2;
static final int STREAM_ID = 3;
static final int STREAM_VERSION = 4;
static final int PROBE_METADATA = 5;
static final int PROBE_DATA = 6;
}
public class ProbesUploader extends Uploader {
@Override
protected String[] getProjection() {
return ProbeQuery.PROJECTION;
}
@Override
protected int getNameIndex() {
return ProbeQuery.OBSERVER_ID;
}
@Override
protected int getVersionIndex() {
return ProbeQuery.OBSERVER_VERSION;
}
@Override
public int addProbe(JsonArray probes, Cursor c) {
JsonObject probe = new JsonObject();
probe.addProperty("stream_id", c.getString(ProbeQuery.STREAM_ID));
probe.addProperty("stream_version", c.getInt(ProbeQuery.STREAM_VERSION));
String data = c.getString(ProbeQuery.PROBE_DATA);
int size = 0;
if (!TextUtils.isEmpty(data)) {
size += data.getBytes().length;
probe.add("data", mParser.parse(data));
}
String metadata = c.getString(ProbeQuery.PROBE_METADATA);
if (!TextUtils.isEmpty(metadata)) {
size += metadata.getBytes().length;
probe.add("metadata", mParser.parse(metadata));
}
probes.add(probe);
return size;
}
@Override
protected void uploadStarted() {
sendBroadcast(new Intent(ProbeUploadService.PROBE_UPLOAD_STARTED));
}
@Override
protected void uploadFinished() {
sendBroadcast(new Intent(ProbeUploadService.PROBE_UPLOAD_FINISHED));
}
@Override
protected void uploadError(String error) {
if (isBackground) {
if (error != null)
NotificationHelper.showProbeUploadErrorNotification(ProbeUploadService.this,
error);
} else {
Intent broadcast = new Intent(ProbeUploadService.PROBE_UPLOAD_ERROR);
if (error != null)
broadcast.putExtra(EXTRA_PROBE_ERROR, error);
sendBroadcast(broadcast);
}
}
@Override
protected Uri getContentURI() {
return Probes.CONTENT_URI;
}
@Override
protected UploadResponse uploadCall(String serverUrl, String username, String password,
String client, String observerId, String observerVersion, JsonArray data) {
return mApi.observerUpload(ConfigHelper.serverUrl(), username, password,
OhmageApi.CLIENT_NAME, observerId, observerVersion, data.toString());
}
@Override
protected String getVersionColumn() {
return Probes.OBSERVER_VERSION;
}
@Override
protected String getNameColumn() {
return Probes.OBSERVER_ID;
}
}
private interface ResponseQuery {
static final String[] PROJECTION = new String[] {
Responses._ID, Responses.CAMPAIGN_URN, Responses.CAMPAIGN_CREATED,
Responses.RESPONSE_DATA
};
static final int CAMPAIGN_URN = 1;
static final int CAMPAIGN_CREATED = 2;
static final int RESPONSE_DATA = 3;
}
public class ResponsesUploader extends Uploader {
@Override
protected String[] getProjection() {
return ResponseQuery.PROJECTION;
}
@Override
protected int getNameIndex() {
return ResponseQuery.CAMPAIGN_URN;
}
@Override
protected int getVersionIndex() {
return ResponseQuery.CAMPAIGN_CREATED;
}
@Override
public int addProbe(JsonArray probes, Cursor c) {
String data = c.getString(ResponseQuery.RESPONSE_DATA);
int size = 0;
if (!TextUtils.isEmpty(data)) {
size += data.getBytes().length;
probes.add(mParser.parse(data));
}
return size;
}
@Override
protected void uploadStarted() {
sendBroadcast(new Intent(ProbeUploadService.RESPONSE_UPLOAD_STARTED));
}
@Override
protected void uploadFinished() {
sendBroadcast(new Intent(ProbeUploadService.RESPONSE_UPLOAD_FINISHED));
}
@Override
protected void uploadError(String error) {
if (isBackground) {
if (error != null)
NotificationHelper.showResponseUploadErrorNotification(ProbeUploadService.this,
error);
} else {
Intent broadcast = new Intent(ProbeUploadService.RESPONSE_UPLOAD_ERROR);
if (error != null)
broadcast.putExtra(EXTRA_PROBE_ERROR, error);
sendBroadcast(broadcast);
}
}
@Override
protected Uri getContentURI() {
return Responses.CONTENT_URI;
}
@Override
protected UploadResponse uploadCall(String serverUrl, String username, String password,
String client, String campaignUrn, String campaignCreated, JsonArray data) {
return mApi.surveyUpload(ConfigHelper.serverUrl(), username, password,
OhmageApi.CLIENT_NAME, campaignUrn, campaignCreated, data.toString());
}
@Override
protected String getVersionColumn() {
return Responses.CAMPAIGN_URN;
}
@Override
protected String getNameColumn() {
return Responses.CAMPAIGN_CREATED;
}
}
}