package com.mobilesorcery.sdk.core.stats;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jface.preference.IPreferenceStore;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import com.mobilesorcery.sdk.core.CoreMoSyncPlugin;
import com.mobilesorcery.sdk.core.MoSyncTool;
import com.mobilesorcery.sdk.core.Util;
/**
* <p>Handles usage statistics. <b>Implementation note:</b> only one eclipse instance will be used for sending stats;
* if another instance is started and run concurrently, no stats will be sent by it.</p>
* We may change this behaviour in the future.
* @author Mattias Bybro
*
*/
public class Stats {
private static final String SEND_INTERVAL_PROP = "send.interval";
public static final long DISABLE_SEND = 0;
public static final long UNASSIGNED_SEND_INTERVAL = -1;
public static final long DEFAULT_SEND_INTERVAL = TimeUnit.MILLISECONDS
.convert(1, TimeUnit.DAYS);
private static final int MAX_UNSENT_SIZE = 3;
private static Stats stats = new Stats();
private Variables variables;
private List<Variables> unsentVariables = new ArrayList<Variables>();
private long lastSendTry;
private long started;
private long sendInterval;
private Timer sendTimer;
private FileLock statsLock;
private FileOutputStream statsLockFileStream;
/**
* The constructor
*/
private Stats() {
}
public static Stats getStats() {
return stats;
}
public void start() {
started = System.currentTimeMillis();
initVariables(true);
try {
setSendInterval(Long.parseLong(MoSyncTool.getDefault().getProperty(
SEND_INTERVAL_PROP)), true);
} catch (Exception e) {
// Ignore.
setSendInterval(UNASSIGNED_SEND_INTERVAL, true);
}
}
private void initVariables(boolean load) {
variables = new Variables();
if (load) {
loadState(false);
}
variables.get(TimeStamp.class, "ide.started").set(started);
variables.get(TimeStamp.class, "stats.collection.started").set();
}
public void stop() throws Exception {
saveState();
if (statsLock != null) {
statsLock.release();
}
Util.safeClose(statsLockFileStream);
}
private IPath getStatsLocation() {
return MoSyncTool.getDefault().getMoSyncHome().append("etc/stats");
}
private void saveState() {
if (!anotherIDEIsRunning()) {
try {
File unsentFile = getStatsLocation().append("unsent.json").toFile();
Util.writeToFile(unsentFile, toString(unsentVariables));
File statsFile = getStatsLocation().append("current.json").toFile();
Util.writeToFile(statsFile, toString(Arrays.asList(variables)));
File lastSendTryFile = getStatsLocation().append("timestamp")
.toFile();
Util.writeToFile(lastSendTryFile, Long.toString(lastSendTry));
} catch (Exception e) {
CoreMoSyncPlugin.getDefault().log(e);
}
}
}
private String toString(List<Variables> variableList) {
JSONArray array = new JSONArray();
for (Variables v : variableList) {
JSONObject o = new JSONObject();
v.write(o);
array.add(o);
}
return array.toJSONString();
}
private List<Variables> fromString(String str) throws Exception {
ArrayList<Variables> vars = new ArrayList<Variables>();
JSONArray array = (JSONArray) new JSONParser().parse(str);
for (int i = 0; i < array.size(); i++) {
JSONObject o = (JSONObject) array.get(i);
Variables v = new Variables();
v.read(o);
vars.add(v);
}
return vars;
}
private void loadState(boolean force) {
if (force || !anotherIDEIsRunning()) {
if (CoreMoSyncPlugin.getDefault().isDebugging()) {
CoreMoSyncPlugin.trace("Loading previously saved usage statistics.");
}
try {
File unsentFile = getStatsLocation().append("unsent.json")
.toFile();
String unsent = unsentFile.exists() ? Util.readFile(unsentFile
.getAbsolutePath()) : null;
File statsFile = getStatsLocation().append("current.json")
.toFile();
String stats = unsentFile.exists() ? Util.readFile(statsFile
.getAbsolutePath()) : null;
File lastSendTryFile = getStatsLocation().append("timestamp")
.toFile();
String lastSendTryStr = lastSendTryFile.exists() ? Util
.readFile(lastSendTryFile.getAbsolutePath()) : null;
lastSendTry = System.currentTimeMillis();
try {
if (!Util.isEmpty(lastSendTryStr)) {
lastSendTry = Long.parseLong(lastSendTryStr.trim());
}
} catch (Exception e) {
// Ignore.
}
if (!Util.isEmpty(stats)) {
List<Variables> variableList = fromString(stats);
if (variableList.size() > 0) {
variables = variableList.get(0);
}
}
if (!Util.isEmpty(unsent)) {
unsentVariables = fromString(unsent);
}
} catch (Exception e) {
CoreMoSyncPlugin.getDefault().log(e);
}
}
}
public Variables getVariables() {
return variables;
}
private void send() {
if (sendInterval == DISABLE_SEND
|| sendInterval == UNASSIGNED_SEND_INTERVAL) {
return;
}
// There may be several workspaces -- but only one is meant for sending.
if (anotherIDEIsRunning()) {
return;
}
if (CoreMoSyncPlugin.getDefault().isDebugging()) {
CoreMoSyncPlugin.trace("Sending stats at " + new Date());
}
// If failed, log and retain these variables until next try...
// And we'll try at next startup regardless.
addToUnsent(variables);
final Variables variablesToSend = variables;
variablesToSend.get(TimeStamp.class, "send.time").set();
initVariables(false);
try {
lastSendTry = System.currentTimeMillis();
ArrayList<Variables> sendThese = new ArrayList<Variables>();
sendThese.addAll(unsentVariables);
sendThese.add(variablesToSend);
String stats = Stats.this.toString(sendThese);
CoreMoSyncPlugin.getDefault().getUpdater().sendStats(stats);
// Now clear!
unsentVariables.clear();
saveState();
} catch (Exception e) {
e.printStackTrace();
}
}
public boolean anotherIDEIsRunning() {
// If we already have the lock, then we know we're it
// Otherwise, we will try to get the file lock.
// The lock is released once the stop method has been called.
if (statsLock != null) {
return false;
}
try {
statsLockFileStream = null;
File lockFile = getStatsLocation().append(".lock").toFile();
if (!lockFile.exists()) {
lockFile.getParentFile().mkdirs();
}
statsLockFileStream = new FileOutputStream(lockFile);
statsLock = statsLockFileStream.getChannel().tryLock();
if (CoreMoSyncPlugin.getDefault().isDebugging()) {
String lockState = statsLock == null ? "Could not lock" : "Locked";
CoreMoSyncPlugin.trace(lockState + " stats file @" + new Date());
}
if (statsLock != null) {
loadState(true);
}
return statsLock == null;
} catch (Exception e) {
return true; // Ok, maybe not but we don't know
}
}
public String getContentsToSend() {
ArrayList<Variables> sendThese = new ArrayList<Variables>();
sendThese.addAll(unsentVariables);
sendThese.add(variables);
return toString(sendThese);
}
private void clear() {
clearUnsent();
initVariables(false);
saveState();
}
private void addToUnsent(Variables v) {
synchronized (unsentVariables) {
unsentVariables.add(v);
if (unsentVariables.size() > MAX_UNSENT_SIZE) {
unsentVariables.remove(0);
}
}
}
private void clearUnsent() {
synchronized (unsentVariables) {
unsentVariables.clear();
}
}
public long getSendInterval() {
return sendInterval;
}
public void setSendInterval(long sendInterval) {
setSendInterval(sendInterval, false);
}
private void setSendInterval(long sendInterval, boolean init) {
long previousInterval = this.sendInterval;
if (previousInterval != sendInterval) {
MoSyncTool.getDefault().setProperty(SEND_INTERVAL_PROP,
Long.toString(sendInterval));
this.sendInterval = sendInterval;
}
boolean wasDisabled = previousInterval == 0;
stopTimer();
if (sendInterval == DISABLE_SEND) {
clear();
} else {
if (wasDisabled && !init) {
// Don't try right away.
lastSendTry = System.currentTimeMillis();
}
startTimer(sendInterval);
}
}
private long timeSinceLastSendTry() {
return System.currentTimeMillis() - lastSendTry;
}
private void startTimer(long interval) {
stopTimer();
if (interval > 0) {
sendTimer = new Timer();
long delay = Math.max(0, interval - timeSinceLastSendTry());
sendTimer.schedule(new TimerTask() {
@Override
public void run() {
send();
}
}, delay, interval);
}
}
private void stopTimer() {
if (sendTimer != null) {
sendTimer.cancel();
}
}
}